From 5fd18c2b56171501ab2890f9f7eb18fcf407c046 Mon Sep 17 00:00:00 2001 From: Mark <163640647+kash1dd@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:30:28 +0300 Subject: [PATCH 1/6] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=83=D1=81=D1=82=D0=B0=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B5=20=D1=80=D0=B5=D0=B0=D0=BA=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/telegram/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram/router.py b/src/telegram/router.py index 8d0db19..00b1de1 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 From aa166c1d51789c3aacbb6ee39fefc7bc80d12b7a Mon Sep 17 00:00:00 2001 From: Mark <163640647+kash1dd@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:39:13 +0300 Subject: [PATCH 2/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=81=D1=82=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA?= =?UTF-8?q?=D0=B8=D1=85=20=D1=81=D1=82=D0=B8=D0=BA=D0=B5=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20=D1=87=D0=B0=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/telegram/router.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/telegram/router.py b/src/telegram/router.py index 00b1de1..4c198c9 100644 --- a/src/telegram/router.py +++ b/src/telegram/router.py @@ -60,8 +60,14 @@ async def send_to_funpay_chat(message: Message, chat_sync_registry: Registry, hu 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 From 1cf843fb5cb808d81e0ce4007ae2ad9e6cf2a079 Mon Sep 17 00:00:00 2001 From: Mark <163640647+kash1dd@users.noreply.github.com> Date: Tue, 21 Apr 2026 02:40:15 +0300 Subject: [PATCH 3/6] =?UTF-8?q?=D0=9F=D0=BE=D1=84=D0=B8=D0=BA=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=20SyntaxWarning=20=D0=B2=20=D1=85=D1=8D=D0=BD=D0=B4?= =?UTF-8?q?=D0=BB=D0=B5=D1=80=D0=B5=20send=5Fto=5Ffunpay=5Fchat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/telegram/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/telegram/router.py b/src/telegram/router.py index 4c198c9..6b8094c 100644 --- a/src/telegram/router.py +++ b/src/telegram/router.py @@ -53,7 +53,7 @@ async def setup_chat_sync_chat( await message.answer('✅ Sync-чат установлен.') -@r.message(~Command(re.compile('\S+')), need_to_resend) +@r.message(~Command(re.compile(r'\S+')), need_to_resend) async def send_to_funpay_chat(message: Message, chat_sync_registry: Registry, hub: FunPayHub): funpay_chat_id = chat_sync_registry.tg_to_fp_pairs[message.message_thread_id] From 43dc8db581dcd69a0c6be32d0ce159173c7ef47d Mon Sep 17 00:00:00 2001 From: Mark <163640647+kash1dd@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:02:09 +0300 Subject: [PATCH 4/6] =?UTF-8?q?=D0=9C=D0=B5=D1=82=D0=BE=D0=B4=20BotRotater?= =?UTF-8?q?.remove=5Fbot=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D1=81=D0=B5?= =?UTF-8?q?=D1=81=D1=81=D0=B8=D1=8E=20=D0=B1=D0=BE=D1=82=D0=B0,=20=D0=B2?= =?UTF-8?q?=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B8=20=D1=81=20=D1=87=D0=B5=D0=BC?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D1=88=D0=BB=D0=BE=D1=81=D1=8C=20=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B0=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=BC;=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20FP?= =?UTF-8?q?H=20=D1=80=D0=BE=D1=83=D1=82=D0=B5=D1=80;=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D1=85=D1=8D=D0=BD=D0=B4=D0=BB?= =?UTF-8?q?=D0=B5=D1=80=20=D0=BD=D0=B0=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=BE=D0=B2=20=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=B2=20(=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20/=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=B7=20BotRotater)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fph/__init__.py | 6 ++++++ src/fph/router.py | 27 +++++++++++++++++++++++++++ src/plugin.py | 6 ++++++ src/types.py | 3 ++- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/fph/__init__.py create mode 100644 src/fph/router.py 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..6336f58 --- /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 = set(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/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/types.py b/src/types.py index d77b8a3..652020c 100644 --- a/src/types.py +++ b/src/types.py @@ -158,13 +158,14 @@ 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) From d52b9a6f545c55584a3b3538fe19f634b52571fa Mon Sep 17 00:00:00 2001 From: Mark <163640647+kash1dd@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:02:44 +0300 Subject: [PATCH 5/6] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ From 394cb32d8c168ccb429adbaf2b7d7e13091e5a95 Mon Sep 17 00:00:00 2001 From: Mark <163640647+kash1dd@users.noreply.github.com> Date: Tue, 21 Apr 2026 03:16:55 +0300 Subject: [PATCH 6/6] =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=B0=D0=BD=D0=B4=D0=B5=D1=80=D1=8B=20=5F=5Fiter=5F=5F?= =?UTF-8?q?=20/=20=5F=5Fnext=5F=5F=20=D0=B8=D0=B7=20BotRotater;=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=20BotRotater.snapshot;=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=B2=D0=BE=D0=B9=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=BE=20BotRotater.tokens;=20send=5Fmessage=5Ftask?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BF=D0=B5=D1=80=D1=8C=20=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BD=D0=B0=D0=BF=D1=88=D0=BE=D1=82=D1=83,=20=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=B1=D0=B5=D1=81=D0=BA=D0=BE=D0=BD=D0=B5=D1=87?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D1=86=D0=B8=D0=BA=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fph/router.py | 2 +- src/funpay/router.py | 13 ++++++------- src/logger.py | 6 ++++++ src/telegram/router.py | 10 +++++++--- src/types.py | 28 ++++++++++++++++------------ 5 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 src/logger.py diff --git a/src/fph/router.py b/src/fph/router.py index 6336f58..a217587 100644 --- a/src/fph/router.py +++ b/src/fph/router.py @@ -18,7 +18,7 @@ async def sync_bot_tokens( chat_sync_rotater: BotRotater, ) -> None: new_tokens = set(parameter.value) - current_tokens = set(chat_sync_rotater._tokens) + current_tokens = chat_sync_rotater.tokens for token in new_tokens - current_tokens: chat_sync_rotater.add_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/telegram/router.py b/src/telegram/router.py index 6b8094c..d516b71 100644 --- a/src/telegram/router.py +++ b/src/telegram/router.py @@ -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-чат уже установлен. ' @@ -54,7 +54,11 @@ async def setup_chat_sync_chat( @r.message(~Command(re.compile(r'\S+')), need_to_resend) -async def send_to_funpay_chat(message: Message, chat_sync_registry: Registry, hub: FunPayHub): +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 @@ -86,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 652020c..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: @@ -167,8 +170,9 @@ async def remove_bot(self, token: str) -> None: 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)