-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
37417b7
commit 5fb1b5d
Showing
35 changed files
with
968 additions
and
591 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
**/__pycache__/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
/venv/ | ||
.idea/ | ||
/.env | ||
/docker-compose.yml | ||
/settings.toml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,13 @@ | ||
# Separate build image | ||
FROM python:3.9-slim-bullseye as compile-image | ||
RUN python -m venv /opt/venv | ||
ENV PATH="/opt/venv/bin:$PATH" | ||
FROM python:3.12-slim AS builder | ||
COPY requirements.txt . | ||
RUN pip install --no-cache-dir --upgrade pip \ | ||
&& pip install --no-cache-dir -r requirements.txt | ||
|
||
# Final image | ||
FROM python:3.9-slim-bullseye | ||
COPY --from=compile-image /opt/venv /opt/venv | ||
ENV PATH="/opt/venv/bin:$PATH" | ||
FROM python:3.12-slim | ||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3/site-packages | ||
ENV PYTHONPATH=/usr/local/lib/python3/site-packages | ||
WORKDIR /app | ||
COPY bot /app/bot | ||
CMD ["python", "-m", "bot"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,78 +1,64 @@ | ||
import asyncio | ||
import logging | ||
|
||
import structlog | ||
from aiogram import Bot, Dispatcher | ||
from aiogram.client.default import DefaultBotProperties | ||
from aiogram.enums import ParseMode | ||
from aiogram.exceptions import TelegramAPIError | ||
from aiogram.types import BotCommand, BotCommandScopeChat | ||
from structlog.typing import FilteringBoundLogger | ||
|
||
from bot.before_start import fetch_admins, check_rights_and_permissions | ||
from bot.config_reader import config | ||
from bot.handlers import setup_routers | ||
from bot.localization import Lang | ||
|
||
|
||
async def set_bot_commands(bot: Bot, main_group_id: int): | ||
commands = [ | ||
BotCommand(command="report", description="Report message to group admins"), | ||
] | ||
await bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=main_group_id)) | ||
from bot.before_start import check_bot_rights_main_group, check_bot_rights_reports_group, fetch_admins, set_bot_commands | ||
from bot.config_reader import get_config, LogConfig, BotConfig | ||
from bot.fluent_loader import get_fluent_localization | ||
from bot.handlers import get_routers | ||
from bot.logs import get_structlog_config | ||
|
||
|
||
async def main(): | ||
logging.basicConfig( | ||
level=logging.INFO, | ||
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s", | ||
) | ||
log_config: LogConfig = get_config(model=LogConfig, root_key="logs") | ||
structlog.configure(**get_structlog_config(log_config)) | ||
|
||
# Define bot, dispatcher and include routers to dispatcher | ||
bot = Bot(token=config.bot_token.get_secret_value(), parse_mode="HTML") | ||
dp = Dispatcher() | ||
bot_config: BotConfig = get_config(model=BotConfig, root_key="bot") | ||
bot = Bot( | ||
token=bot_config.token.get_secret_value(), | ||
default=DefaultBotProperties(parse_mode=ParseMode.HTML), | ||
) | ||
|
||
# Check that bot is admin in "main" group and has necessary permissions | ||
try: | ||
await check_rights_and_permissions(bot, config.group_main) | ||
except (TelegramAPIError, PermissionError) as error: | ||
error_msg = f"Error with main group: {error}" | ||
try: | ||
await bot.send_message(config.group_reports, error_msg) | ||
finally: | ||
print(error_msg) | ||
return | ||
logger: FilteringBoundLogger = structlog.get_logger() | ||
|
||
# Collect admins so that we don't have to fetch them every time | ||
# Check that bot can run properly | ||
try: | ||
result = await fetch_admins(bot) | ||
except TelegramAPIError as error: | ||
error_msg = f"Error fetching main group admins: {error}" | ||
try: | ||
await bot.send_message(config.group_reports, error_msg) | ||
finally: | ||
print(error_msg) | ||
return | ||
config.admins = result | ||
await check_bot_rights_main_group(bot, bot_config.main_group_id) | ||
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() | ||
return | ||
|
||
try: | ||
lang = Lang(config.lang) | ||
except ValueError: | ||
print(f"Error no localization found for language code: {config.lang}") | ||
await check_bot_rights_reports_group(bot, bot_config.reports_group_id) | ||
except (TelegramAPIError, PermissionError) as ex: | ||
await logger.aerror(f"Cannot use bot in reports group, because {ex.__class__.__name__}: {str(ex)}") | ||
await bot.session.close() | ||
return | ||
|
||
# Register handlers | ||
router = setup_routers() | ||
dp.include_router(router) | ||
await set_bot_commands(bot, bot_config.main_group_id) | ||
|
||
main_group_admins: dict = await fetch_admins(bot, bot_config.main_group_id) | ||
|
||
# Register /-commands in UI | ||
await set_bot_commands(bot, config.group_main) | ||
l10n = get_fluent_localization() | ||
|
||
logging.info("Starting bot") | ||
dp = Dispatcher( | ||
admins=main_group_admins, | ||
bot_config=bot_config, | ||
l10n=l10n, | ||
) | ||
dp.include_routers(*get_routers( | ||
main_group_id=bot_config.main_group_id, | ||
reports_group_id=bot_config.reports_group_id, | ||
)) | ||
|
||
# Start polling | ||
# await bot.get_updates(offset=-1) # skip pending updates (optional) | ||
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types(), lang=lang) | ||
|
||
await logger.ainfo("Program started...") | ||
await dp.start_polling(bot) | ||
|
||
if __name__ == '__main__': | ||
try: | ||
asyncio.run(main()) | ||
except (KeyboardInterrupt, SystemExit): | ||
logging.error("Bot stopped!") | ||
asyncio.run(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,53 @@ | ||
from typing import Dict, List, Union | ||
|
||
from aiogram import Bot | ||
from aiogram.types import ChatMemberAdministrator, ChatMemberOwner | ||
from aiogram.types import ( | ||
ChatMemberAdministrator, ChatMemberOwner, ChatMemberBanned, ChatMemberLeft, ChatMemberRestricted, | ||
BotCommand, BotCommandScopeChat | ||
) | ||
|
||
|
||
async def check_bot_rights_main_group( | ||
bot: Bot, | ||
main_group_id: int, | ||
): | ||
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") | ||
|
||
from bot.config_reader import config | ||
async def check_bot_rights_reports_group( | ||
bot: Bot, | ||
reports_group_id: int, | ||
): | ||
chat_member_info = await bot.get_chat_member( | ||
chat_id=reports_group_id, user_id=bot.id | ||
) | ||
if isinstance(chat_member_info, (ChatMemberLeft, ChatMemberBanned)): | ||
raise PermissionError("bot is banned from the group") | ||
if isinstance(chat_member_info, ChatMemberRestricted) and not chat_member_info.can_send_messages: | ||
raise PermissionError("bot is not allowed to send messages") | ||
|
||
|
||
async def fetch_admins(bot: Bot) -> Dict: | ||
async def fetch_admins( | ||
bot: Bot, | ||
main_group_id: int, | ||
) -> dict: | ||
result = {} | ||
admins: List[Union[ChatMemberOwner, ChatMemberAdministrator]] | ||
admins = await bot.get_chat_administrators(config.group_main) | ||
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} | ||
return result | ||
|
||
|
||
async def check_rights_and_permissions(bot: Bot, chat_id: int): | ||
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("Bot is not an administrator") | ||
if not chat_member_info.can_restrict_members or not chat_member_info.can_delete_messages: | ||
raise PermissionError("Bot needs 'restrict participants' and 'delete messages' permissions to work properly") | ||
|
||
async def set_bot_commands(bot: Bot, main_group_id: int): | ||
commands = [ | ||
BotCommand(command="report", description="Report message to group admins"), | ||
] | ||
await bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=main_group_id)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,13 @@ | ||
from aiogram.dispatcher.filters.callback_data import CallbackData | ||
from enum import Enum | ||
|
||
from aiogram.filters.callback_data import CallbackData | ||
|
||
class DeleteMsgCallback(CallbackData, prefix="delmsg"): | ||
# action to perform: "del" to simply delete a message or "ban" to ban chat/user as well | ||
action: str | ||
# the ID of chat or user to perform action on | ||
entity_id: int | ||
# string-formatted list of message IDs to delete | ||
message_ids: str # Lists are not supported =( | ||
|
||
class AdminAction(str, Enum): | ||
DELETE = "del" | ||
BAN = "ban" | ||
|
||
class AdminActionCallbackV1(CallbackData, prefix="v1"): | ||
action: AdminAction | ||
user_or_chat_id: int | ||
reported_message_id: int |
Oops, something went wrong.