Files
OpenOPC/opc/channels/telegram.py
T
2026-07-01 17:56:31 +08:00

130 lines
4.7 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any
from loguru import logger
from opc.channels.provider_base import PollingChannel
from opc.core.models import SystemMessage
_TELEGRAM_MAX_MESSAGE_LEN = 4000
class TelegramChannel(PollingChannel):
name = "telegram"
required_package = "telegram"
def __init__(self, config: Any, bus: Any):
super().__init__(config, bus)
self._bot: Any = None
self._update_offset: int = 0
def get_required_config_fields(self) -> list[str]:
return ["token"]
def get_poll_interval_seconds(self) -> float:
return 0.25
def normalize_update(self, payload: dict[str, Any]) -> dict[str, Any]:
message = payload.get("message", payload)
chat = message.get("chat", {})
user = message.get("from", {})
attachments = []
for key in ("photo", "document", "audio", "voice", "video"):
if message.get(key):
attachments.append({"type": key, "value": message.get(key)})
text = str(message.get("text", "") or message.get("caption", "") or "")
return {
"sender_id": str(user.get("id", "")),
"chat_id": str(chat.get("id", "")),
"content": text,
"thread_id": str(message.get("message_thread_id", "") or ""),
"reply_to": str((message.get("reply_to_message") or {}).get("message_id", "") or ""),
"attachments": attachments,
"metadata": {
"message_id": str(message.get("message_id", "") or ""),
"chat_type": str(chat.get("type", "") or ""),
},
}
async def _ensure_bot(self) -> Any:
if self._bot is not None:
return self._bot
from telegram import Bot
self._bot = Bot(token=self.config.token)
return self._bot
async def poll_once(self) -> None:
bot = await self._ensure_bot()
updates = await bot.get_updates(
offset=self._update_offset or None,
timeout=20,
allowed_updates=["message"],
)
for update in updates:
self._update_offset = max(self._update_offset, int(update.update_id) + 1)
message = getattr(update, "message", None)
if message is None:
continue
payload = self.normalize_update(update.to_dict())
await self.publish_normalized(payload)
async def send(self, message: SystemMessage) -> None:
await super().send(message)
bot = await self._ensure_bot()
metadata = dict(message.metadata or {})
chat_id = int(str(metadata.get("chat_id") or message.session_id))
thread_id = str(metadata.get("thread_id", "") or "")
reply_to = str(metadata.get("reply_to", "") or "")
reply_to_message_id = None
if reply_to:
try:
reply_to_message_id = int(reply_to)
except ValueError:
reply_to_message_id = None
for chunk in _split_message(message.content or "", _TELEGRAM_MAX_MESSAGE_LEN):
kwargs: dict[str, Any] = {"chat_id": chat_id, "text": chunk}
if thread_id:
try:
kwargs["message_thread_id"] = int(thread_id)
except ValueError:
pass
if self.config.reply_to_message and reply_to_message_id is not None:
kwargs["reply_to_message_id"] = reply_to_message_id
await bot.send_message(**kwargs)
for attachment in list(metadata.get("attachments", []) or []):
if not isinstance(attachment, str):
continue
path = Path(attachment)
if not path.is_file():
logger.warning("telegram attachment path not found: {}", path)
continue
with path.open("rb") as handle:
kwargs = {"chat_id": chat_id, "document": handle}
if thread_id:
try:
kwargs["message_thread_id"] = int(thread_id)
except ValueError:
pass
if self.config.reply_to_message and reply_to_message_id is not None:
kwargs["reply_to_message_id"] = reply_to_message_id
await bot.send_document(**kwargs)
def _split_message(text: str, max_len: int) -> list[str]:
if not text:
return []
chunks: list[str] = []
remaining = text
while len(remaining) > max_len:
split_at = remaining.rfind("\n", 0, max_len)
if split_at <= 0:
split_at = max_len
chunks.append(remaining[:split_at].rstrip())
remaining = remaining[split_at:].lstrip()
if remaining:
chunks.append(remaining)
return chunks