diff --git a/bot/__main__.py b/bot/__main__.py index 09c3d0f..6c8fc5b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -28,7 +28,7 @@ async def main(): # Check that bot can run properly try: - await check_bot_rights_main_group(bot, bot_config.main_group_id) + await check_bot_rights_main_group(bot, bot_config) except (TelegramAPIError, PermissionError) as ex: await logger.aerror(f"Cannot use bot in main group, because {ex.__class__.__name__}: {str(ex)}") await bot.session.close() diff --git a/bot/before_start.py b/bot/before_start.py index 5ef3803..c800c12 100644 --- a/bot/before_start.py +++ b/bot/before_start.py @@ -1,21 +1,24 @@ from aiogram import Bot from aiogram.types import ( ChatMemberAdministrator, ChatMemberOwner, ChatMemberBanned, ChatMemberLeft, ChatMemberRestricted, - BotCommand, BotCommandScopeChat + BotCommand, BotCommandScopeAllGroupChats ) +from bot.config_reader import BotConfig + async def check_bot_rights_main_group( bot: Bot, - main_group_id: int, + bot_config: BotConfig, ): - chat_member_info = await bot.get_chat_member( - chat_id=main_group_id, user_id=bot.id - ) - if not isinstance(chat_member_info, ChatMemberAdministrator): - raise PermissionError("bot must be an administrator to work properly") - if not chat_member_info.can_restrict_members or not chat_member_info.can_delete_messages: - raise PermissionError("bot needs 'ban users' and 'delete messages' permissions to work properly") + for chat_id in bot_config.main_group_id: + chat_member_info = await bot.get_chat_member( + chat_id=chat_id, user_id=bot.id + ) + if not isinstance(chat_member_info, ChatMemberAdministrator): + raise PermissionError(f"Chat {chat_id}: bot must be an administrator to work properly") + if not chat_member_info.can_restrict_members or not chat_member_info.can_delete_messages: + raise PermissionError(f"Chat {chat_id}: bot needs 'ban users' and 'delete messages' permissions to work properly") async def check_bot_rights_reports_group( bot: Bot, @@ -32,22 +35,26 @@ async def check_bot_rights_reports_group( async def fetch_admins( bot: Bot, - main_group_id: int, -) -> dict: + main_group_id: tuple[int, ...], +) -> dict[int, dict[int, object]]: result = {} - admins: list[ChatMemberOwner | ChatMemberAdministrator] = await bot.get_chat_administrators(main_group_id) - for admin in admins: - if admin.user.is_bot: - continue - if isinstance(admin, ChatMemberOwner): - result[admin.user.id] = {"can_restrict_members": True} - else: - result[admin.user.id] = {"can_restrict_members": admin.can_restrict_members} + + for chat_id in main_group_id: + result[chat_id] = {} + chat_admins: list[ChatMemberOwner | ChatMemberAdministrator] = await bot.get_chat_administrators(chat_id) + for admin in chat_admins: + if admin.user.is_bot: + continue + if isinstance(admin, ChatMemberOwner): + result[chat_id][admin.user.id] = {"can_restrict_members": True} + else: + result[chat_id][admin.user.id] = {"can_restrict_members": admin.can_restrict_members} + return result -async def set_bot_commands(bot: Bot, main_group_id: int): +async def set_bot_commands(bot: Bot): commands = [ BotCommand(command="report", description="Report message to group admins"), ] - await bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=main_group_id)) + await bot.set_my_commands(commands, scope=BotCommandScopeAllGroupChats()) diff --git a/bot/callback_factories.py b/bot/callback_factories.py index c6af6db..7f3fdc0 100644 --- a/bot/callback_factories.py +++ b/bot/callback_factories.py @@ -9,5 +9,6 @@ class AdminAction(str, Enum): class AdminActionCallbackV1(CallbackData, prefix="v1"): action: AdminAction + chat_id: int user_or_chat_id: int reported_message_id: int diff --git a/bot/config_reader.py b/bot/config_reader.py index 3ee6eaa..1516abe 100644 --- a/bot/config_reader.py +++ b/bot/config_reader.py @@ -2,7 +2,7 @@ from functools import lru_cache from os import getenv from tomllib import load -from typing import Type, TypeVar +from typing import Type, TypeVar, Union from pydantic import BaseModel, SecretStr, field_validator @@ -16,7 +16,7 @@ class LogRenderer(StrEnum): class BotConfig(BaseModel): token: SecretStr - main_group_id: int + main_group_id: tuple[int, ...] reports_group_id: int utc_offset: int date_format: str @@ -24,6 +24,13 @@ class BotConfig(BaseModel): remove_joins: bool auto_ban_channels: bool + @field_validator("main_group_id", mode="before") + @classmethod + def main_group_id_transform(cls, raw: Union[int, tuple[int, ...]]) -> tuple[int, ...]: + if isinstance(raw, list): + return tuple(raw) + return (raw,) + class LogConfig(BaseModel): show_datetime: bool diff --git a/bot/handlers/__init__.py b/bot/handlers/__init__.py index 2c367f8..825db8b 100644 --- a/bot/handlers/__init__.py +++ b/bot/handlers/__init__.py @@ -2,12 +2,12 @@ from . import changing_admins, reporting_to_admins, reacting_to_reports, restricting_users, additional_features def get_routers( - main_group_id: int, + main_group_id: tuple[int, ...], reports_group_id: int, ) -> list[Router]: main_group_router = Router() - main_group_router.message.filter(F.chat.id == main_group_id) - main_group_router.chat_member.filter(F.chat.id == main_group_id) + main_group_router.message.filter(F.chat.id.in_(main_group_id)) + main_group_router.chat_member.filter(F.chat.id.in_(main_group_id)) main_group_router.include_routers( changing_admins.router, restricting_users.router, diff --git a/bot/handlers/changing_admins.py b/bot/handlers/changing_admins.py index 9e6321f..a3234fb 100644 --- a/bot/handlers/changing_admins.py +++ b/bot/handlers/changing_admins.py @@ -12,7 +12,7 @@ @router.chat_member(AdminAdded()) async def admin_added( event: types.ChatMemberUpdated, - admins: dict + admins: dict[int, dict[int, object]] ): """ Handle "new admin was added" event and update admins dictionary @@ -25,15 +25,15 @@ async def admin_added( can_restrict_members = True else: can_restrict_members = new.can_restrict_members - admins[new.user.id] = {"can_restrict_members": can_restrict_members} - await logger.ainfo(f"Added new admin with id={new.user.id} and {can_restrict_members=}") + admins[event.chat.id][new.user.id] = {"can_restrict_members": can_restrict_members} + await logger.ainfo(f"Added new admin with id={new.user.id} and {can_restrict_members=} to chat {event.chat.id}") @router.chat_member(AdminRemoved()) async def admin_removed( event: types.ChatMemberUpdated, - admins: dict, + admins: dict[int, dict[int, object]], ): """ Handle "user was demoted from admins" event and update admins dictionary @@ -42,6 +42,6 @@ async def admin_removed( :param admins: dictionary of admins before handling this event """ new = event.new_chat_member - if new.user.id in admins.keys(): - del admins[new.user.id] - await logger.ainfo(f"Removed user with id={new.user.id} from admins") + if new.user.id in admins[event.chat.id].keys(): + del admins[event.chat.id][new.user.id] + await logger.ainfo(f"Removed user with id={new.user.id} from admins of chat {event.chat.id}") diff --git a/bot/handlers/reacting_to_reports.py b/bot/handlers/reacting_to_reports.py index da0abf3..ed5ad04 100644 --- a/bot/handlers/reacting_to_reports.py +++ b/bot/handlers/reacting_to_reports.py @@ -27,7 +27,7 @@ async def reacting_to_reports( # First, try to delete message try: await bot.delete_message( - chat_id=bot_config.main_group_id, + chat_id=callback_data.chat_id, message_id=callback_data.reported_message_id, ) except TelegramAPIError as ex: @@ -51,7 +51,7 @@ async def reacting_to_reports( # If action was delete and ban, let's try to ban user as well offender_ban_success: bool = True - args = {"chat_id": bot_config.main_group_id} + args = {"chat_id": callback_data.chat_id} if callback_data.user_or_chat_id > 0: func = bot.ban_chat_member args.update(user_id=callback_data.user_or_chat_id) diff --git a/bot/handlers/reporting_to_admins.py b/bot/handlers/reporting_to_admins.py index c507dd2..5819b6c 100644 --- a/bot/handlers/reporting_to_admins.py +++ b/bot/handlers/reporting_to_admins.py @@ -17,13 +17,13 @@ async def cmd_report( message: Message, command: CommandObject, bot: Bot, - admins: dict, + admins: dict[int, dict[int, object]], bot_config: BotConfig, l10n: FluentLocalization, replied_msg: Message, ): # If message sent by user who is admin - if message.reply_to_message.from_user.id in admins: + if message.reply_to_message.from_user.id in admins[message.chat.id]: await message.reply(l10n.format_value("error-cannot-report-admins")) return # If message sent by anonymous group admin @@ -36,6 +36,7 @@ async def cmd_report( return # Gather all report message parameters + chat_id = replied_msg.chat.id offender_id = replied_msg.sender_chat.id if replied_msg.sender_chat else replied_msg.from_user.id offender_message_id = replied_msg.message_id formatted_date_time_offset = get_formatted_datetime(bot_config) @@ -57,6 +58,7 @@ async def cmd_report( chat_id=bot_config.reports_group_id, text=l10n.format_value(locale_key, params), reply_markup=get_report_keyboard( + chat_id=chat_id, user_or_chat_id=offender_id, reported_message_id=offender_message_id, l10n=l10n, diff --git a/bot/handlers/restricting_users.py b/bot/handlers/restricting_users.py index 551ca17..849c683 100644 --- a/bot/handlers/restricting_users.py +++ b/bot/handlers/restricting_users.py @@ -34,17 +34,17 @@ async def cmd_ro( bot: Bot, bot_config: BotConfig, l10n: FluentLocalization, - admins: dict, + admins: dict[int, dict[int, object]], ): # Prohibit non-admins from using this command - if message.from_user.id not in admins: + if message.from_user.id not in admins[message.chat.id]: return # Prohibit from restricting admins - if message.reply_to_message.from_user.id in admins.keys(): + if message.reply_to_message.from_user.id in admins[message.chat.id].keys(): await message.reply(l10n.format_value("error-restricting-admin")) return # Do not allow admin with no restrict permissions from restricting other users - if admins.get(message.from_user.id, {}).get("can_restrict_members", False) is False: + if admins[message.chat.id].get(message.from_user.id, {}).get("can_restrict_members", False) is False: await message.reply(l10n.format_value("error-no-restrict-permissions")) return # If a message is sent on behalf of channel, then we can only ban it diff --git a/bot/keyboards.py b/bot/keyboards.py index 10ec55d..4615836 100644 --- a/bot/keyboards.py +++ b/bot/keyboards.py @@ -10,6 +10,7 @@ def get_report_keyboard( + chat_id: int, user_or_chat_id: int, reported_message_id: int, l10n: FluentLocalization, @@ -18,6 +19,7 @@ def get_report_keyboard( delete_callback = AdminActionCallbackV1( action=AdminAction.DELETE, + chat_id=chat_id, user_or_chat_id=user_or_chat_id, reported_message_id=reported_message_id, ) @@ -29,6 +31,7 @@ def get_report_keyboard( ban_callback = AdminActionCallbackV1( action=AdminAction.BAN, + chat_id=chat_id, user_or_chat_id=user_or_chat_id, reported_message_id=reported_message_id, )