Files
tracer-cloud--opensre/gateway/polling/telegram_poller/poller.py
T
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

116 lines
3.8 KiB
Python

"""Long-polling transport for local Telegram gateway development."""
from __future__ import annotations
import json
import logging
import time
from typing import Any
import httpx
from gateway.config.get_gateway_settings import TelegramInboundMessage
from gateway.polling.telegram_poller.parse_telegram_update import parse_update
logger = logging.getLogger(__name__)
_API = "https://api.telegram.org/bot{token}/getUpdates"
_CONFLICT_ERROR_CODE = 409
_DEFAULT_RETRY_SECONDS = 2.0
_MAX_CONFLICT_BACKOFF_SECONDS = 30.0
_WARNING_COOLDOWN_SECONDS = 60.0
def _decode_telegram_response(response: httpx.Response) -> dict[str, Any]:
"""Parse a Telegram Bot API JSON body regardless of HTTP status."""
try:
data = response.json()
except json.JSONDecodeError:
return {}
return data if isinstance(data, dict) else {}
class TelegramPoller:
"""Poll Telegram getUpdates and yield normalized inbound messages."""
def __init__(self, bot_token: str, *, timeout: int = 30) -> None:
self._token = bot_token
self._timeout = timeout
self._offset = 0
self._conflict_backoff_seconds = _DEFAULT_RETRY_SECONDS
self._last_warning_monotonic = 0.0
def poll_once(self) -> list[TelegramInboundMessage]:
url = _API.format(token=self._token)
params: dict[str, str | int | list[str]] = {
"timeout": self._timeout,
"offset": self._offset + 1,
"allowed_updates": ["message"],
}
try:
response = httpx.get(url, params=params, timeout=float(self._timeout + 5))
except Exception as exc:
self._log_transient("[telegram-gateway] getUpdates failed: %s", exc)
time.sleep(_DEFAULT_RETRY_SECONDS)
return []
data = _decode_telegram_response(response)
if not data.get("ok"):
self._handle_poll_error(data=data, response=response)
return []
self._conflict_backoff_seconds = _DEFAULT_RETRY_SECONDS
result = data.get("result")
if not isinstance(result, list):
return []
events: list[TelegramInboundMessage] = []
for raw in result:
if not isinstance(raw, dict):
continue
update_id = int(raw.get("update_id") or 0)
self._offset = max(self._offset, update_id)
parsed = parse_update(raw)
if parsed is not None:
events.append(parsed)
return events
def _handle_poll_error(
self,
*,
data: dict[str, Any],
response: httpx.Response,
) -> None:
error_code = data.get("error_code")
description = str(
data.get("description") or response.text.strip() or f"HTTP {response.status_code}"
)
if error_code == _CONFLICT_ERROR_CODE:
logger.debug(
"[telegram-gateway] getUpdates conflict (retry in %.0fs): %s",
self._conflict_backoff_seconds,
description,
)
time.sleep(self._conflict_backoff_seconds)
self._conflict_backoff_seconds = min(
self._conflict_backoff_seconds * 2,
_MAX_CONFLICT_BACKOFF_SECONDS,
)
return
self._log_transient(
"[telegram-gateway] getUpdates not ok (HTTP %s, error_code=%s): %s",
response.status_code,
error_code,
description,
)
time.sleep(_DEFAULT_RETRY_SECONDS)
def _log_transient(self, message: str, *args: object) -> None:
now = time.monotonic()
if now - self._last_warning_monotonic < _WARNING_COOLDOWN_SECONDS:
logger.debug(message, *args)
return
self._last_warning_monotonic = now
logger.warning(message, *args)