"""Telegram bot transport and update handling.""" import json import os import sys import time import urllib.request from tai_pkg.consts import Consts from tai_pkg.errors import BackendError from tai_pkg.sandbox import Sandbox from tai_pkg.scheduler import Scheduler from tai_pkg.text import Text __all__ = ["Telegram", "TelegramBot"] class Telegram: def __init__(self, token): self.token = token def call(self, method, params, timeout=70): body = json.dumps(params).encode("utf-8") request = urllib.request.Request(Consts.TELEGRAM_API + self.token + "/" + method, data=body, headers={"Content-Type": "application/json"}, method="POST") with urllib.request.urlopen(request, timeout=timeout) as response: return json.loads(response.read().decode("utf-8", "replace")) class TelegramBot: """TelegramBot.""" @staticmethod def telegram_env_path(home): return os.path.join(home, "telegram.env") @staticmethod def load_telegram_token(home): token = os.environ.get("TELEGRAM_BOT_TOKEN") or "" if token: return token path = TelegramBot.telegram_env_path(home) if not os.path.exists(path): return "" with open(path, "r", encoding="utf-8") as handle: for line in handle.read().splitlines(): if line.startswith("TELEGRAM_BOT_TOKEN="): return line.split("=", 1)[1].strip().strip("'\"") return "" @staticmethod def telegram_send(bot, chat_id, text): for pos in range(0, len(text), 4000): bot.call("sendMessage", {"chat_id": chat_id, "text": text[pos:pos + 4000] or "(empty reply)"}) @staticmethod def telegram_transcribe(bot, file_id): try: info = bot.call("getFile", {"file_id": file_id}) remote = (info.get("result") or {}).get("file_path", "") if not remote: return "error: telegram returned no file" with urllib.request.urlopen(Consts.TELEGRAM_FILE_API + bot.token + "/" + remote, timeout=60) as response: audio = response.read(20000000) engine = Sandbox.ensure_box() return Sandbox.box_transcribe(engine, audio) except (OSError, ValueError, BackendError) as exc: return "error: transcription failed: " + Text.short_error(exc) @staticmethod def handle_telegram_update(agent, bot, update): message = update.get("message") or {} chat_id = (message.get("chat") or {}).get("id") if not chat_id: return text = message.get("text") or "" if text == "/start": TelegramBot.telegram_send(bot, chat_id, "tai online. Send any message, /new for a fresh start.") return if text == "/new": agent.reset_history() TelegramBot.telegram_send(bot, chat_id, "fresh start.") return voice = message.get("voice") or message.get("audio") if voice: bot.call("sendChatAction", {"chat_id": chat_id, "action": "typing"}) text = TelegramBot.telegram_transcribe(bot, voice.get("file_id", "")) if text.startswith("error"): TelegramBot.telegram_send(bot, chat_id, text) return if not text.strip(): return bot.call("sendChatAction", {"chat_id": chat_id, "action": "typing"}) reply = agent.run_turn(text, capture=True) TelegramBot.telegram_send(bot, chat_id, reply) @staticmethod def run_telegram_bot(agent): token = TelegramBot.load_telegram_token(agent.config.home) if not token: print("missing TELEGRAM_BOT_TOKEN", file=sys.stderr) return 2 bot = Telegram(token) try: me = bot.call("getMe", {}) except (OSError, ValueError) as exc: print("telegram auth failed: %s" % Text.short_error(exc), file=sys.stderr) return 2 print("telegram bot online as @%s" % (me.get("result") or {}).get("username", "?")) Scheduler.start_scheduler(agent.config, agent.store.seal) offset = 0 while True: try: data = bot.call("getUpdates", {"offset": offset, "timeout": 50}) except (OSError, ValueError) as exc: print("poll error: %s, retrying" % Text.short_error(exc)) time.sleep(5) continue for update in data.get("result") or []: offset = update.get("update_id", offset) + 1 try: TelegramBot.handle_telegram_update(agent, bot, update) except Exception as exc: print("update error: %s" % Text.short_error(exc))