Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple chats support #13

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
49 changes: 28 additions & 21 deletions bot/before_start.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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())
1 change: 1 addition & 0 deletions bot/callback_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 9 additions & 2 deletions bot/config_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -16,14 +16,21 @@ 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
time_format: str
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
Expand Down
6 changes: 3 additions & 3 deletions bot/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 7 additions & 7 deletions bot/handlers/changing_admins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}")
4 changes: 2 additions & 2 deletions bot/handlers/reacting_to_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions bot/handlers/reporting_to_admins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions bot/handlers/restricting_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions bot/keyboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@


def get_report_keyboard(
chat_id: int,
user_or_chat_id: int,
reported_message_id: int,
l10n: FluentLocalization,
Expand All @@ -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,
)
Expand All @@ -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,
)
Expand Down