130 lines
4.7 KiB
Python
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
|