Files
tai/tai_pkg/telegram.py
T
retoor 8df5cf7a45 Split monolithic tai.py into tai_pkg package, keep tai-orig.py as original
tai.py (8643 lines) is now a thin facade over tai_pkg/ (33 feature modules, one class or feature per file). tai-orig.py preserves the pre-split single-file version byte for byte (sha256 verified against snapshot); import tai and ./tai.py behavior unchanged.

Equivalence proof, all run on this tree immediately pre-commit: differential battery tai-orig.py vs facade 3718 checks 0 failures (surface, pure fuzz, tool dispatch, store/audit, release ops, fake-HOME install roundtrip, CLI runs); test_tai.py 261 OK; test_seal.py 16 OK; --version/--help byte-identical; dir(tai) zero removals.

Fix included: opencode wire-protocol schema echo leaked into replies; opencode_parse now scans with JSONDecoder.raw_decode capped at 200 scans (13 OpencodeParseTests). Test count 261 = 237 HEAD + 8 pre-existing uncommitted ToolArgumentsTests (now committed with this change, disclosed) + 13 OpencodeParseTests + 3 EmbeddedStringsTests. Patch-target-only test migration (24 hunks), zero assertion changes. tai_pkg clean-regenerated after mid-verification tamper was found and reverted.
2026-10-10 19:25:02 +02:00

124 lines
4.7 KiB
Python

"""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))