diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/src/fph/__init__.py b/src/fph/__init__.py new file mode 100644 index 0000000..dee94ef --- /dev/null +++ b/src/fph/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from .router import router + + +__all__ = ['router'] diff --git a/src/fph/router.py b/src/fph/router.py new file mode 100644 index 0000000..a217587 --- /dev/null +++ b/src/fph/router.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from funpayhub.lib.properties import ListParameter + +from funpayhub.app.dispatching import Router + +from ..types import BotRotater + + +router = Router(name='chat_sync') + + +@router.on_parameter_value_changed( + lambda parameter, plugin_properties: parameter is plugin_properties.bot_tokens, +) +async def sync_bot_tokens( + parameter: ListParameter[str], + chat_sync_rotater: BotRotater, +) -> None: + new_tokens = set(parameter.value) + current_tokens = chat_sync_rotater.tokens + + for token in new_tokens - current_tokens: + chat_sync_rotater.add_bot(token) + + for token in current_tokens - new_tokens: + await chat_sync_rotater.remove_bot(token) diff --git a/src/funpay/router.py b/src/funpay/router.py index 7205c55..2c5900b 100644 --- a/src/funpay/router.py +++ b/src/funpay/router.py @@ -12,6 +12,8 @@ from funpayhub.app.telegram.ui.ids import MenuIds from funpayhub.app.telegram.ui.builders.context import NewMessageMenuContext +from ..logger import logger + if TYPE_CHECKING: from aiogram import Bot as TGBot @@ -99,12 +101,7 @@ async def sync_new_message( async def send_message_task(chat_id: int, thread_id: int, rotater: BotRotater, menu: Menu) -> None: - while True: - try: - bot = rotater.next_bot() - except StopIteration: - return - + for bot in rotater.snapshot(): try: await bot.send_message( chat_id=chat_id, @@ -113,4 +110,6 @@ async def send_message_task(chat_id: int, thread_id: int, rotater: BotRotater, m ) return except TelegramUnauthorizedError: - rotater.remove_bot(bot.token) + await rotater.remove_bot(bot.token) + except Exception: + logger.exception('Failed to send message to %d (%d thread)', chat_id, thread_id) diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000..6c76baa --- /dev/null +++ b/src/logger.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from funpayhub.app.plugin.plugin import get_plugin_logger + + +logger = get_plugin_logger('com.github.qvvonk.funpayhub.chat_sync_plugin') diff --git a/src/plugin.py b/src/plugin.py index 162f762..f64bfa3 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -7,6 +7,7 @@ from funpayhub.app.plugin import Plugin from .types import Registry, BotRotater +from .fph.router import router as chat_sync_hub_router from .properties import ChatSyncProperties from .funpay.router import router as chat_sync_fp_router from .telegram.router import router as chat_sync_tg_router @@ -16,6 +17,8 @@ from aiogram import Router as TGRouter from funpaybotengine import Router as FPRouter + from funpayhub.app.dispatching import Router as HubRouter + class ChatSyncPlugin(Plugin): _registry: Registry | None = None @@ -28,6 +31,9 @@ async def pre_setup(self) -> None: async def properties(self) -> ChatSyncProperties: return ChatSyncProperties() + async def hub_routers(self) -> HubRouter: + return chat_sync_hub_router + async def funpay_routers(self) -> FPRouter: return chat_sync_fp_router diff --git a/src/telegram/router.py b/src/telegram/router.py index 8d0db19..d516b71 100644 --- a/src/telegram/router.py +++ b/src/telegram/router.py @@ -6,12 +6,12 @@ from io import BytesIO from aiogram import Router +from aiogram.types import Message, ReactionTypeEmoji from aiogram.filters import Command from funpaybotengine.exceptions import FunPayBotEngineError if TYPE_CHECKING: - from aiogram.types import Message, ReactionTypeEmoji from chat_sync.src.types import Registry from chat_sync.src.properties import ChatSyncProperties @@ -37,7 +37,7 @@ async def need_to_resend( async def setup_chat_sync_chat( message: Message, plugin_properties: ChatSyncProperties, -): +) -> None: if plugin_properties.sync_chat_id.value: await message.answer( '❌ Sync-чат уже установлен. ' @@ -53,15 +53,25 @@ async def setup_chat_sync_chat( await message.answer('✅ Sync-чат установлен.') -@r.message(~Command(re.compile('\S+')), need_to_resend) -async def send_to_funpay_chat(message: Message, chat_sync_registry: Registry, hub: FunPayHub): +@r.message(~Command(re.compile(r'\S+')), need_to_resend) +async def send_to_funpay_chat( + message: Message, + chat_sync_registry: Registry, + hub: FunPayHub, +) -> None: funpay_chat_id = chat_sync_registry.tg_to_fp_pairs[message.message_thread_id] image: BytesIO | None = None text: str | None = None - if message.photo: - file = await message.bot.get_file(message.photo[-1].file_id) + if message.photo or message.sticker: + if message.sticker and (message.sticker.is_animated or message.sticker.is_video): + emoji = ReactionTypeEmoji(emoji='🗿') + await message.react(reaction=[emoji], is_big=True) + return + file = await message.bot.get_file( + message.photo[-1].file_id if message.photo else message.sticker.file_id + ) buffer = BytesIO() await message.bot.download_file(file.file_path, buffer) image = buffer @@ -80,7 +90,7 @@ async def send_to_funpay_chat(message: Message, chat_sync_registry: Registry, hu try: emoji = ReactionTypeEmoji(emoji='🎉') await message.react(reaction=[emoji], is_big=True) - except: + except Exception: pass except FunPayBotEngineError: emoji = ReactionTypeEmoji(emoji='💩') diff --git a/src/types.py b/src/types.py index d77b8a3..0a6ad74 100644 --- a/src/types.py +++ b/src/types.py @@ -3,7 +3,7 @@ import json from types import MappingProxyType from pathlib import Path -from collections.abc import Iterator +from collections.abc import Iterable from aiogram import Bot from aiogram.enums import ParseMode @@ -122,9 +122,9 @@ def path(self) -> Path: class BotRotater: """ - Циклический итератор Telegram ботов. + Round-robin пул Telegram ботов. - Хранит набор ботов и поочерёдно возвращает их по кругу (round-robin). + Хранит набор ботов и поочерёдно возвращает их по кругу через `next_bot()`. Поддерживает добавление и удаление ботов во время работы. """ @@ -134,14 +134,17 @@ class BotRotater: link_preview_is_disabled=True, ) - def __init__(self, tokens) -> None: - self._tokens = set(tokens) - self._bots = [self._bot_from_token(token) for token in self._tokens] + def __init__(self, tokens: Iterable[str]) -> None: + self._tokens: set[str] = set(tokens) + self._bots: list[Bot] = [self._bot_from_token(token) for token in self._tokens] self._current_bot_index = 0 - def __next__(self) -> Bot: + def next_bot(self) -> Bot | None: + """ + Возвращает следующего бота по кругу или `None`, если ботов нет. + """ if not self._bots: - raise StopIteration + return None if self._current_bot_index > len(self._bots) - 1: self._current_bot_index = 0 @@ -149,8 +152,8 @@ def __next__(self) -> Bot: self._current_bot_index += 1 return bot - def __iter__(self) -> Iterator[Bot]: - return self + def snapshot(self) -> list[Bot]: + return list(self._bots) def add_bot(self, token: str) -> None: if token in self._tokens: @@ -158,16 +161,18 @@ def add_bot(self, token: str) -> None: self._tokens.add(token) self._bots.append(self._bot_from_token(token)) - def remove_bot(self, token: str) -> None: + async def remove_bot(self, token: str) -> None: if token not in self._tokens: return self._tokens.discard(token) for i in self._bots: if i.token == token: self._bots.remove(i) + await i.session.close() - def next_bot(self) -> Bot: - return next(self) + @property + def tokens(self) -> frozenset[str]: + return frozenset(self._tokens) def _bot_from_token(self, token: str) -> Bot: return Bot(token=token, default=self._DEFAULT_BOT_PROPERTIES)