mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
_compute_file_range now caps any resolved window at _FILE_MAX_LINES instead of only the exact whole-file shape, closing the near-whole-file bypass. Telegram sends ride a generalized after-commit outbox (defer_after_commit over the F107 machinery) so a slow Bot API can no longer hold the caller's transaction open; TelegramClient grows an abstract close(). The KB update loop iterates AUTO_INDEX_DIRS so docs/map edits re-index without a restart. PR-label application catches all exceptions per its never-raises contract, and pr_merge's CEO-only message names the resolved branch.
122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
"""Telegram Bot API client — server-side only, agents never touch it.
|
|
|
|
Thin httpx wrapper for the one operation the V1 bridge needs: send a message
|
|
to a chat. Mirrors ``services/x_client.py``'s ``NullXClient`` shape — a
|
|
``NullTelegramClient`` is returned when credentials are unset, so the
|
|
notification fan-out degrades gracefully (no-op, no exception) exactly like an
|
|
unconfigured X client — never raises into the caller, never makes a network
|
|
call.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING
|
|
|
|
import httpx
|
|
|
|
if TYPE_CHECKING:
|
|
from roboco.services.telegram_credentials import TelegramCredentialsData
|
|
|
|
_API_BASE = "https://api.telegram.org"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TelegramSendResult:
|
|
"""Outcome of a ``send_message`` call."""
|
|
|
|
sent: bool
|
|
detail: str = ""
|
|
|
|
|
|
class TelegramClient(ABC):
|
|
"""Abstract Telegram API surface. ``NullTelegramClient`` is the
|
|
graceful-degradation stub."""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def configured(self) -> bool: ...
|
|
|
|
@abstractmethod
|
|
async def send_message(self, text: str) -> TelegramSendResult: ...
|
|
|
|
@abstractmethod
|
|
async def close(self) -> None:
|
|
"""Release transport resources; no-op when no transport exists."""
|
|
|
|
|
|
class NullTelegramClient(TelegramClient):
|
|
"""No credentials configured — every call is a no-op, never raises."""
|
|
|
|
async def close(self) -> None:
|
|
return None
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return False
|
|
|
|
async def send_message(self, text: str) -> TelegramSendResult:
|
|
_ = text
|
|
return TelegramSendResult(sent=False, detail="no credentials configured")
|
|
|
|
|
|
class LiveTelegramClient(TelegramClient):
|
|
"""Real Bot API ``sendMessage`` call."""
|
|
|
|
def __init__(
|
|
self,
|
|
creds: TelegramCredentialsData,
|
|
*,
|
|
timeout: float,
|
|
client: httpx.AsyncClient | None = None,
|
|
) -> None:
|
|
self._creds = creds
|
|
self._timeout = timeout
|
|
self._client = client
|
|
self._owns_client = client is None
|
|
|
|
@property
|
|
def configured(self) -> bool:
|
|
return True
|
|
|
|
async def _http(self) -> httpx.AsyncClient:
|
|
if self._client is None:
|
|
self._client = httpx.AsyncClient(timeout=self._timeout)
|
|
return self._client
|
|
|
|
async def close(self) -> None:
|
|
if self._owns_client and self._client is not None:
|
|
await self._client.aclose()
|
|
self._client = None
|
|
|
|
async def send_message(self, text: str) -> TelegramSendResult:
|
|
url = f"{_API_BASE}/bot{self._creds.bot_token}/sendMessage"
|
|
client = await self._http()
|
|
try:
|
|
resp = await client.post(
|
|
url,
|
|
json={"chat_id": self._creds.chat_id, "text": text},
|
|
timeout=self._timeout,
|
|
)
|
|
except httpx.HTTPError as exc:
|
|
return TelegramSendResult(sent=False, detail=f"network error: {exc}")
|
|
if not resp.is_success:
|
|
return TelegramSendResult(
|
|
sent=False,
|
|
detail=f"HTTP {resp.status_code}: {resp.text[:200]}",
|
|
)
|
|
return TelegramSendResult(sent=True)
|
|
|
|
|
|
def build_telegram_client(
|
|
creds: TelegramCredentialsData | None,
|
|
*,
|
|
timeout: float,
|
|
client: httpx.AsyncClient | None = None,
|
|
) -> TelegramClient:
|
|
"""Construct the client — ``NullTelegramClient`` when credentials are unset."""
|
|
if creds is None:
|
|
return NullTelegramClient()
|
|
return LiveTelegramClient(creds, timeout=timeout, client=client)
|