Files
dzentra_bot/app/src/telegram/live/runner.py

271 lines
7.6 KiB
Python

# app/src/telegram/live/runner.py
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from typing import Callable
from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest
@dataclass(slots=True)
class LiveScreen:
# имя live-экрана: market / portfolio
screen: str
# Telegram bot instance
bot: Bot
# чат, где находится live-экран
chat_id: int
# сообщение, которое нужно автообновлять
message_id: int
# функция сборки текста экрана
render_text: Callable[[], str]
# функция сборки клавиатуры экрана
render_markup: Callable[[], object]
# интервал обновления в секундах
interval_seconds: int = 5
@dataclass(slots=True)
class StaticScreen:
# имя статичного экрана: journal / monitoring / etc
screen: str
# Telegram bot instance
bot: Bot
# чат, где находится экран
chat_id: int
# сообщение экрана
message_id: int
class ScreenRegistry:
_screens: dict[str, list[StaticScreen]] = {}
# зарегистрировать статичный экран
@classmethod
def register_screen(cls, static_screen: StaticScreen) -> None:
screens = cls._screens.setdefault(static_screen.screen, [])
screens[:] = [
item
for item in screens
if not (
item.chat_id == static_screen.chat_id
and item.message_id == static_screen.message_id
)
]
screens.append(static_screen)
# удалить конкретное сообщение из всех статичных экранов без удаления из Telegram
@classmethod
def unregister_message(
cls,
*,
chat_id: int,
message_id: int,
) -> None:
empty_screens: list[str] = []
for screen, screens in cls._screens.items():
screens[:] = [
item
for item in screens
if not (
item.chat_id == chat_id
and item.message_id == message_id
)
]
if not screens:
empty_screens.append(screen)
for screen in empty_screens:
cls._screens.pop(screen, None)
# удалить старые статичные экраны указанного типа
@classmethod
async def delete_screen(
cls,
*,
screen: str,
bot: Bot,
chat_id: int,
) -> None:
screens = cls._screens.get(screen, [])
remaining: list[StaticScreen] = []
for static_screen in screens:
if static_screen.chat_id != chat_id:
remaining.append(static_screen)
continue
try:
await bot.delete_message(
chat_id=static_screen.chat_id,
message_id=static_screen.message_id,
)
except Exception:
pass
if remaining:
cls._screens[screen] = remaining
else:
cls._screens.pop(screen, None)
class LiveScreenRunner:
_screens: dict[str, list[LiveScreen]] = {}
_tasks: dict[str, asyncio.Task] = {}
# зарегистрировать live-экран
@classmethod
def register_screen(cls, live_screen: LiveScreen) -> None:
screens = cls._screens.setdefault(live_screen.screen, [])
screens[:] = [
item
for item in screens
if not (
item.chat_id == live_screen.chat_id
and item.message_id == live_screen.message_id
)
]
screens.append(live_screen)
# удалить конкретное сообщение из всех live-экранов без удаления из Telegram
@classmethod
def unregister_message(
cls,
*,
chat_id: int,
message_id: int,
) -> None:
empty_screens: list[str] = []
for screen, screens in cls._screens.items():
screens[:] = [
item
for item in screens
if not (
item.chat_id == chat_id
and item.message_id == message_id
)
]
if not screens:
empty_screens.append(screen)
for screen in empty_screens:
cls._screens.pop(screen, None)
# удалить все live-экраны указанного типа из Telegram
@classmethod
async def delete_screen(
cls,
*,
screen: str,
bot: Bot,
chat_id: int,
) -> None:
screens = cls._screens.get(screen, [])
remaining: list[LiveScreen] = []
for live_screen in screens:
if live_screen.chat_id != chat_id:
remaining.append(live_screen)
continue
try:
await bot.delete_message(
chat_id=live_screen.chat_id,
message_id=live_screen.message_id,
)
except Exception:
pass
if remaining:
cls._screens[screen] = remaining
else:
cls._screens.pop(screen, None)
# запустить автообновление группы экранов
@classmethod
def start(cls, screen: str) -> None:
task = cls._tasks.get(screen)
if task is not None and not task.done():
return
cls._tasks[screen] = asyncio.create_task(cls._worker(screen))
# остановить автообновление группы экранов
@classmethod
def stop(cls, screen: str) -> None:
task = cls._tasks.get(screen)
if task is None:
return
task.cancel()
cls._tasks.pop(screen, None)
# фоновый цикл обновления группы экранов
@classmethod
async def _worker(cls, screen: str) -> None:
while True:
await cls._refresh_screen(screen)
await asyncio.sleep(cls._screen_interval(screen))
# получить интервал обновления группы экранов
@classmethod
def _screen_interval(cls, screen: str) -> int:
screens = cls._screens.get(screen, [])
if not screens:
return 5
return screens[0].interval_seconds
# обновить все Telegram-сообщения live-экрана
@classmethod
async def _refresh_screen(cls, screen: str) -> None:
screens = cls._screens.get(screen, [])
if not screens:
return
alive_screens: list[LiveScreen] = []
for live_screen in screens:
try:
await live_screen.bot.edit_message_text(
chat_id=live_screen.chat_id,
message_id=live_screen.message_id,
text=live_screen.render_text(),
reply_markup=live_screen.render_markup(),
)
alive_screens.append(live_screen)
except TelegramBadRequest as exc:
if "message is not modified" in str(exc).lower():
alive_screens.append(live_screen)
continue
except Exception:
pass
if alive_screens:
cls._screens[screen] = alive_screens
else:
cls._screens.pop(screen, None)