From 0ae337711ebe6e0e60e781395a2386fd0741dce2 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Sun, 25 Oct 2026 02:13:04 +0500 Subject: [PATCH] =?UTF-8?q?=D0=91=D0=BE=D1=82=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BB=20=D1=80=D0=B5=D0=B0=D0=B3=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20=D0=BD=D0=B0=20=D1=87=D1=83?= =?UTF-8?q?=D0=B6=D0=B8=D0=B5=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20?= =?UTF-8?q?=D0=B8=20=D1=84=D0=BE=D1=80=D0=B2=D0=B0=D1=80=D0=B4=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=D1=85=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сообщение в чате уведомлений оказалось форвардом из группы «Никитка играется с ботом», где тестовый бот был администратором: он видел сообщение прод-бота, считал отправителя посторонним и присылал access_denied с форвардом. Теперь чужие группы и каналы игнорируются полностью — и сообщения, и выходы, и callback-кнопки; личные чаты и основной чат работают как раньше. --- README.md | 2 +- bot/handlers/member_events.py | 3 +++ bot/handlers/shop.py | 10 ++++++++++ bot/services/access.py | 10 ++++++++++ common/version.py | 2 +- tests/test_access_chat.py | 23 +++++++++++++++++++++++ tests/test_version.py | 2 +- 7 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 tests/test_access_chat.py diff --git a/README.md b/README.md index 33933bd..ed9a439 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ make backup E=test # на домашнем сервере: ./scripts/clickho ## Версионирование -- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.5.0`. +- Единый источник версии — `common/version.py` (`__version__`), текущая — `0.5.1`. - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`). - Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3). - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. diff --git a/bot/handlers/member_events.py b/bot/handlers/member_events.py index c80c92d..e11c11d 100644 --- a/bot/handlers/member_events.py +++ b/bot/handlers/member_events.py @@ -98,6 +98,9 @@ async def _send_fallback_welcome(event: ChatMemberUpdated, user: User) -> None: @router.chat_member(ChatMemberUpdatedFilter(member_status_changed=LEAVE_TRANSITION)) async def goodbye_handler(event: ChatMemberUpdated) -> None: """Пользователь вышел или был исключён — удалить данные именно его.""" + if event.chat.id != get_settings().chat_id: + return + leaver = event.old_chat_member.user if leaver.is_bot: return diff --git a/bot/handlers/shop.py b/bot/handlers/shop.py index a249be9..b7126be 100644 --- a/bot/handlers/shop.py +++ b/bot/handlers/shop.py @@ -11,6 +11,7 @@ from aiogram.types import CallbackQuery, Message from bot.keyboards import get_keyboard_donat, get_keyboard_main, get_keyboard_title from bot.services import access from bot.services import shop as shop_service +from common.config import get_settings from common.texts import system from common.texts.render import render from common.texts.store import get_text @@ -41,6 +42,15 @@ async def _render_chapter(chapter: str) -> tuple[str, object] | None: @router.callback_query() async def callbacks_handler(callback: CallbackQuery) -> None: + settings = get_settings() + if ( + callback.message is not None + and callback.message.chat.type != "private" + and callback.message.chat.id != settings.chat_id + ): + await callback.answer() + return + answer_text = "" args = (callback.data or "").split("_") action = args[0] if args else "" diff --git a/bot/services/access.py b/bot/services/access.py index b29d31c..f4fc643 100644 --- a/bot/services/access.py +++ b/bot/services/access.py @@ -25,6 +25,12 @@ _ALLOWED_STATUSES = { ChatMemberStatus.RESTRICTED, } + +def is_allowed_chat(chat_id: int, chat_type: str, main_chat_id: int) -> bool: + """Личные чаты и основной чат; чужие группы/каналы игнорируются полностью.""" + return chat_type == "private" or chat_id == main_chat_id + + #: TTL-кеш членства, чтобы не дёргать get_chat_member на каждое сообщение. _MEMBERSHIP_TTL = 300.0 _membership_cache: dict[int, tuple[float, bool]] = {} @@ -50,6 +56,10 @@ async def check_user(message: Message) -> bool: if user is None: return False + # Сообщения из посторонних групп не обрабатываем и не шлём по ним уведомления. + if not is_allowed_chat(message.chat.id, message.chat.type, settings.chat_id): + return False + if user.id in settings.admin_ids or await _is_member(message.bot, user.id): await users_service.register_from_message(message) remember_user(user.id, user.username) diff --git a/common/version.py b/common/version.py index d64ee27..32eafd1 100644 --- a/common/version.py +++ b/common/version.py @@ -7,4 +7,4 @@ from __future__ import annotations -__version__ = "0.5.0" +__version__ = "0.5.1" diff --git a/tests/test_access_chat.py b/tests/test_access_chat.py new file mode 100644 index 0000000..76fd8c0 --- /dev/null +++ b/tests/test_access_chat.py @@ -0,0 +1,23 @@ +"""Тесты фильтра чатов: бот игнорирует чужие группы.""" + +from __future__ import annotations + +from bot.services.access import is_allowed_chat + + +def test_private_chat_is_allowed() -> None: + assert is_allowed_chat(123, "private", 100) is True + + +def test_main_chat_is_allowed() -> None: + assert is_allowed_chat(100, "supergroup", 100) is True + assert is_allowed_chat(100, "group", 100) is True + + +def test_foreign_group_is_ignored() -> None: + assert is_allowed_chat(200, "supergroup", 100) is False + assert is_allowed_chat(200, "group", 100) is False + + +def test_foreign_channel_is_ignored() -> None: + assert is_allowed_chat(200, "channel", 100) is False diff --git a/tests/test_version.py b/tests/test_version.py index 20b7125..37244eb 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,4 +7,4 @@ from common import __version__ def test_version_is_set(): assert __version__ - assert __version__ == "0.5.0" + assert __version__ == "0.5.1"