from __future__ import annotations import logging from typing import TYPE_CHECKING from aiogram import Bot from bot.ha_client import HaClient from bot.storage import LinkedUser, Storage if TYPE_CHECKING: pass logger = logging.getLogger(__name__) NOTICE_ROLES = frozenset({"notice", "character"}) TG_MAX_LEN = 4096 def split_telegram_message(text: str, limit: int = TG_MAX_LEN) -> list[str]: if len(text) <= limit: return [text] chunks: list[str] = [] remaining = text while remaining: if len(remaining) <= limit: chunks.append(remaining) break split_at = remaining.rfind("\n", 0, limit) if split_at <= 0: split_at = limit chunks.append(remaining[:split_at]) remaining = remaining[split_at:].lstrip("\n") return chunks async def send_text(bot: Bot, chat_id: int, text: str) -> None: for chunk in split_telegram_message(text): await bot.send_message(chat_id, chunk) async def advance_cursors( storage: Storage, client: HaClient, user: LinkedUser, ) -> None: sessions = await client.list_sessions() for session in sessions: session_id = int(session["id"]) after_id = await storage.get_last_message_id(user.telegram_id, session_id) page = await client.get_messages(session_id, after_id=after_id or None, limit=100) messages = page.get("messages") or [] max_id = after_id for message in messages: msg_id = int(message["id"]) max_id = max(max_id, msg_id) if max_id > after_id: await storage.set_last_message_id(user.telegram_id, session_id, max_id) async def sync_notices_for_user( bot: Bot, storage: Storage, ha_base_url: str, user: LinkedUser, *, send: bool = True, ) -> None: client = HaClient(ha_base_url, user.api_token) try: reminders = await client.get_reminders_snapshot() pomodoro = await client.get_pomodoro_status() except Exception: logger.exception("Failed to fetch notify seq for telegram_id=%s", user.telegram_id) reminders = {} pomodoro = {} reminder_seq = int(reminders.get("notify_seq") or 0) pomodoro_seq = int((pomodoro.get("cycle") or {}).get("chat_notify_seq") or 0) sessions = await client.list_sessions() pending: list[tuple[int, str]] = [] for session in sessions: session_id = int(session["id"]) after_id = await storage.get_last_message_id(user.telegram_id, session_id) try: page = await client.get_messages(session_id, after_id=after_id or None, limit=100) except Exception: logger.exception( "Failed to fetch messages session_id=%s telegram_id=%s", session_id, user.telegram_id, ) continue messages = page.get("messages") or [] max_id = after_id for message in messages: msg_id = int(message["id"]) max_id = max(max_id, msg_id) role = str(message.get("role") or "") if role in NOTICE_ROLES and send: content = str(message.get("content") or "").strip() if content: pending.append((msg_id, content)) if max_id > after_id: await storage.set_last_message_id(user.telegram_id, session_id, max_id) if send: pending.sort(key=lambda item: item[0]) for _, content in pending: try: await send_text(bot, user.telegram_id, content) except Exception: logger.exception("Failed to send notice to telegram_id=%s", user.telegram_id) await storage.update_seq( user.telegram_id, reminder_seq=reminder_seq, pomodoro_seq=pomodoro_seq, ) async def run_notify_worker( bot: Bot, storage: Storage, ha_base_url: str, poll_interval_sec: int, ) -> None: import asyncio logger.info("Notify worker started (interval=%ss)", poll_interval_sec) while True: users = await storage.list_linked_users() for user in users: try: await sync_notices_for_user(bot, storage, ha_base_url, user, send=True) except Exception: logger.exception("Notify sync failed for telegram_id=%s", user.telegram_id) await asyncio.sleep(poll_interval_sec)