Бот перестал реагировать на чужие группы и форвардить их сообщения

Сообщение в чате уведомлений оказалось форвардом из группы «Никитка играется с ботом», где тестовый бот был администратором: он видел сообщение прод-бота, считал отправителя посторонним и присылал access_denied с форвардом. Теперь чужие группы и каналы игнорируются полностью — и сообщения, и выходы, и callback-кнопки; личные чаты и основной чат работают как раньше.
This commit is contained in:
MpakobecEx 2026-10-25 02:13:04 +05:00
parent e125d4bb0e
commit 058dfd6cc4
7 changed files with 49 additions and 3 deletions

View File

@ -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`). - Схема — SemVer-подобная (`MAJOR.MINOR.PATCH`); git-тег = `v` + версия (`v0.1`).
- Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3). - Docker-образы тегируются версией (Блок 2), события ClickHouse получают `bot_version` (Блок 3).
- Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`. - Выпуск: изменить `common/version.py` → коммит → аннотированный тег `vX.Y` → `git push --tags`.

View File

@ -98,6 +98,9 @@ async def _send_fallback_welcome(event: ChatMemberUpdated, user: User) -> None:
@router.chat_member(ChatMemberUpdatedFilter(member_status_changed=LEAVE_TRANSITION)) @router.chat_member(ChatMemberUpdatedFilter(member_status_changed=LEAVE_TRANSITION))
async def goodbye_handler(event: ChatMemberUpdated) -> None: async def goodbye_handler(event: ChatMemberUpdated) -> None:
"""Пользователь вышел или был исключён — удалить данные именно его.""" """Пользователь вышел или был исключён — удалить данные именно его."""
if event.chat.id != get_settings().chat_id:
return
leaver = event.old_chat_member.user leaver = event.old_chat_member.user
if leaver.is_bot: if leaver.is_bot:
return return

View File

@ -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.keyboards import get_keyboard_donat, get_keyboard_main, get_keyboard_title
from bot.services import access from bot.services import access
from bot.services import shop as shop_service from bot.services import shop as shop_service
from common.config import get_settings
from common.texts import system from common.texts import system
from common.texts.render import render from common.texts.render import render
from common.texts.store import get_text from common.texts.store import get_text
@ -41,6 +42,15 @@ async def _render_chapter(chapter: str) -> tuple[str, object] | None:
@router.callback_query() @router.callback_query()
async def callbacks_handler(callback: CallbackQuery) -> None: 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 = "" answer_text = ""
args = (callback.data or "").split("_") args = (callback.data or "").split("_")
action = args[0] if args else "" action = args[0] if args else ""

View File

@ -25,6 +25,12 @@ _ALLOWED_STATUSES = {
ChatMemberStatus.RESTRICTED, 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 на каждое сообщение. #: TTL-кеш членства, чтобы не дёргать get_chat_member на каждое сообщение.
_MEMBERSHIP_TTL = 300.0 _MEMBERSHIP_TTL = 300.0
_membership_cache: dict[int, tuple[float, bool]] = {} _membership_cache: dict[int, tuple[float, bool]] = {}
@ -50,6 +56,10 @@ async def check_user(message: Message) -> bool:
if user is None: if user is None:
return False 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): if user.id in settings.admin_ids or await _is_member(message.bot, user.id):
await users_service.register_from_message(message) await users_service.register_from_message(message)
remember_user(user.id, user.username) remember_user(user.id, user.username)

View File

@ -7,4 +7,4 @@
from __future__ import annotations from __future__ import annotations
__version__ = "0.5.0" __version__ = "0.5.1"

23
tests/test_access_chat.py Normal file
View File

@ -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

View File

@ -7,4 +7,4 @@ from common import __version__
def test_version_is_set(): def test_version_is_set():
assert __version__ assert __version__
assert __version__ == "0.5.0" assert __version__ == "0.5.1"