Skip to content

Commit 5fb1b5d

Browse files
2024 Edition (#9)
1 parent 37417b7 commit 5fb1b5d

35 files changed

Lines changed: 968 additions & 591 deletions

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/__pycache__/

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
/venv/
22
.idea/
3-
/.env
43
/docker-compose.yml
4+
/settings.toml

Dockerfile

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
# Separate build image
2-
FROM python:3.9-slim-bullseye as compile-image
3-
RUN python -m venv /opt/venv
4-
ENV PATH="/opt/venv/bin:$PATH"
2+
FROM python:3.12-slim AS builder
53
COPY requirements.txt .
64
RUN pip install --no-cache-dir --upgrade pip \
75
&& pip install --no-cache-dir -r requirements.txt
86

97
# Final image
10-
FROM python:3.9-slim-bullseye
11-
COPY --from=compile-image /opt/venv /opt/venv
12-
ENV PATH="/opt/venv/bin:$PATH"
8+
FROM python:3.12-slim
9+
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3/site-packages
10+
ENV PYTHONPATH=/usr/local/lib/python3/site-packages
1311
WORKDIR /app
1412
COPY bot /app/bot
1513
CMD ["python", "-m", "bot"]

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019-2022 Groosha
3+
Copyright (c) 2019-2024 Aleksandr K (also known as Master_Groosha on GitHub)
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,29 @@ This repository contains source code of a small yet rather powerful bot for Tele
66
Uses [aiogram](https://github.com/aiogram/aiogram) framework.
77
The main goal is to build a bot with no external database needed. Thus, it may lack some features, but hey, it's open source!
88

9+
⚠️ Warning: this bot can be used **ONLY** to notify people (most often, group admins), that someone might
10+
violate group's own rules. If you want to report some content to Telegram official support team, please use "Report"
11+
button in your app.
12+
913
#### Screenshot
1014

1115
![Left - main group. Right - group for admins only. If you don't see this image, please check GitHub repo](screenshots/cover.png)
1216

1317
#### Features
1418
* `/report` command to gather reports from users;
15-
* Reports can be sent to a dedicated chat or to dialogues with admins;
16-
* `/ro` command to set user "read-only" and `/nomedia` to allow text messages only;
19+
* Reports will be sent to a separate group chat;
20+
* `/ro` command to set user "read-only";
1721
* [optional] Automatically remove "user joined" service messages;
1822
* [optional] Automatically ban channels (since
1923
[December 2021](https://telegram.org/blog/protected-content-delete-by-date-and-more#anonymous-posting-in-public-groups)
2024
users can write on behalf of their channels);
21-
* If text message starts with `@admin`, admins are notified;
25+
* If message starts with `@admin` or `@admins`, admins are notified;
2226
* A simple interface for admins to choose one of actions on reported message;
23-
* English and Russian languages are built-in.
27+
* Use any locale you want, examples for English and Russian languages are included.
2428

2529
#### Requirements
26-
* Python 3.9 and above;
27-
* Tested on Linux, should work on Windows, no platform-specific code is used;
28-
* Systemd (you can use it to enable autostart and autorestart) or Docker.
30+
* Python 3.11+ (developed under 3.12);
31+
* Systemd (you can use it to enable autostart and autorestart), Docker or anything else you prefer.
2932

3033
#### Installation
3134
1. Go to [@BotFather](https://t.me/telegram), create a new bot, write down its token, add it to your existing group
@@ -34,13 +37,12 @@ and **make bot an admin**. You also need to give it "Delete messages" permission
3437
**Remember**: anyone who is in that group may perform actions like "Delete", "Ban" and so on, so be careful.
3538
3. Use some bot like [@my_id_bot](https://t.me/my_id_bot) to get IDs of these two groups;
3639
4. Clone this repo and `cd` into it;
37-
5. Copy `env_dist` to `.env` (with dot). **Warning**: files starting with dot may be hidden in Linux,
38-
so don't worry if you stop seeing this file, it's still here!
40+
5. Copy `settings.example.toml` to `settings.toml`.
3941
6. Replace default values with your own;
4042
7. Now choose installation method: **systemd** or **Docker**
4143

4244
##### systemd
43-
1. Create a venv (virtual environment): `python3.9 -m venv venv` (or any other Python 3.7+ version);
45+
1. Create a venv (virtual environment): `python3.12 -m venv venv` (or any other Python 3.11+ version);
4446
2. `source venv/bin/activate && pip install -r requirements.txt`;
4547
3. Rename `reportbot.service.example` to `reportbot.service` and move it to `/etc/systemd/system`;
4648
4. Open that file and change values for `WorkingDirectory`, `ExecStart` and `EnvironmentFile` providing the correct
@@ -49,6 +51,6 @@ path values;
4951
6.Check your bot's status and logs: `systemctl status reportbot.service`.
5052

5153
##### Docker
52-
1. Build and run your container: `docker-compose up -d`.
53-
54-
Alternatively, check [docker-compose.yml](docker-compose.yml) file from this repo.
54+
1. Copy `docker-compose.example.yml` as `docker-compose.yml`.
55+
2. Open it and edit values
56+
3. Build and run your container: `docker-compose up -d`.

bot/__main__.py

Lines changed: 43 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,64 @@
11
import asyncio
2-
import logging
32

3+
import structlog
44
from aiogram import Bot, Dispatcher
5+
from aiogram.client.default import DefaultBotProperties
6+
from aiogram.enums import ParseMode
57
from aiogram.exceptions import TelegramAPIError
6-
from aiogram.types import BotCommand, BotCommandScopeChat
8+
from structlog.typing import FilteringBoundLogger
79

8-
from bot.before_start import fetch_admins, check_rights_and_permissions
9-
from bot.config_reader import config
10-
from bot.handlers import setup_routers
11-
from bot.localization import Lang
12-
13-
14-
async def set_bot_commands(bot: Bot, main_group_id: int):
15-
commands = [
16-
BotCommand(command="report", description="Report message to group admins"),
17-
]
18-
await bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=main_group_id))
10+
from bot.before_start import check_bot_rights_main_group, check_bot_rights_reports_group, fetch_admins, set_bot_commands
11+
from bot.config_reader import get_config, LogConfig, BotConfig
12+
from bot.fluent_loader import get_fluent_localization
13+
from bot.handlers import get_routers
14+
from bot.logs import get_structlog_config
1915

2016

2117
async def main():
22-
logging.basicConfig(
23-
level=logging.INFO,
24-
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
25-
)
18+
log_config: LogConfig = get_config(model=LogConfig, root_key="logs")
19+
structlog.configure(**get_structlog_config(log_config))
2620

27-
# Define bot, dispatcher and include routers to dispatcher
28-
bot = Bot(token=config.bot_token.get_secret_value(), parse_mode="HTML")
29-
dp = Dispatcher()
21+
bot_config: BotConfig = get_config(model=BotConfig, root_key="bot")
22+
bot = Bot(
23+
token=bot_config.token.get_secret_value(),
24+
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
25+
)
3026

31-
# Check that bot is admin in "main" group and has necessary permissions
32-
try:
33-
await check_rights_and_permissions(bot, config.group_main)
34-
except (TelegramAPIError, PermissionError) as error:
35-
error_msg = f"Error with main group: {error}"
36-
try:
37-
await bot.send_message(config.group_reports, error_msg)
38-
finally:
39-
print(error_msg)
40-
return
27+
logger: FilteringBoundLogger = structlog.get_logger()
4128

42-
# Collect admins so that we don't have to fetch them every time
29+
# Check that bot can run properly
4330
try:
44-
result = await fetch_admins(bot)
45-
except TelegramAPIError as error:
46-
error_msg = f"Error fetching main group admins: {error}"
47-
try:
48-
await bot.send_message(config.group_reports, error_msg)
49-
finally:
50-
print(error_msg)
51-
return
52-
config.admins = result
31+
await check_bot_rights_main_group(bot, bot_config.main_group_id)
32+
except (TelegramAPIError, PermissionError) as ex:
33+
await logger.aerror(f"Cannot use bot in main group, because {ex.__class__.__name__}: {str(ex)}")
34+
await bot.session.close()
35+
return
5336

5437
try:
55-
lang = Lang(config.lang)
56-
except ValueError:
57-
print(f"Error no localization found for language code: {config.lang}")
38+
await check_bot_rights_reports_group(bot, bot_config.reports_group_id)
39+
except (TelegramAPIError, PermissionError) as ex:
40+
await logger.aerror(f"Cannot use bot in reports group, because {ex.__class__.__name__}: {str(ex)}")
41+
await bot.session.close()
5842
return
5943

60-
# Register handlers
61-
router = setup_routers()
62-
dp.include_router(router)
44+
await set_bot_commands(bot, bot_config.main_group_id)
45+
46+
main_group_admins: dict = await fetch_admins(bot, bot_config.main_group_id)
6347

64-
# Register /-commands in UI
65-
await set_bot_commands(bot, config.group_main)
48+
l10n = get_fluent_localization()
6649

67-
logging.info("Starting bot")
50+
dp = Dispatcher(
51+
admins=main_group_admins,
52+
bot_config=bot_config,
53+
l10n=l10n,
54+
)
55+
dp.include_routers(*get_routers(
56+
main_group_id=bot_config.main_group_id,
57+
reports_group_id=bot_config.reports_group_id,
58+
))
6859

69-
# Start polling
70-
# await bot.get_updates(offset=-1) # skip pending updates (optional)
71-
await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types(), lang=lang)
7260

61+
await logger.ainfo("Program started...")
62+
await dp.start_polling(bot)
7363

74-
if __name__ == '__main__':
75-
try:
76-
asyncio.run(main())
77-
except (KeyboardInterrupt, SystemExit):
78-
logging.error("Bot stopped!")
64+
asyncio.run(main())

bot/before_start.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,53 @@
1-
from typing import Dict, List, Union
2-
31
from aiogram import Bot
4-
from aiogram.types import ChatMemberAdministrator, ChatMemberOwner
2+
from aiogram.types import (
3+
ChatMemberAdministrator, ChatMemberOwner, ChatMemberBanned, ChatMemberLeft, ChatMemberRestricted,
4+
BotCommand, BotCommandScopeChat
5+
)
6+
7+
8+
async def check_bot_rights_main_group(
9+
bot: Bot,
10+
main_group_id: int,
11+
):
12+
chat_member_info = await bot.get_chat_member(
13+
chat_id=main_group_id, user_id=bot.id
14+
)
15+
if not isinstance(chat_member_info, ChatMemberAdministrator):
16+
raise PermissionError("bot must be an administrator to work properly")
17+
if not chat_member_info.can_restrict_members or not chat_member_info.can_delete_messages:
18+
raise PermissionError("bot needs 'ban users' and 'delete messages' permissions to work properly")
519

6-
from bot.config_reader import config
20+
async def check_bot_rights_reports_group(
21+
bot: Bot,
22+
reports_group_id: int,
23+
):
24+
chat_member_info = await bot.get_chat_member(
25+
chat_id=reports_group_id, user_id=bot.id
26+
)
27+
if isinstance(chat_member_info, (ChatMemberLeft, ChatMemberBanned)):
28+
raise PermissionError("bot is banned from the group")
29+
if isinstance(chat_member_info, ChatMemberRestricted) and not chat_member_info.can_send_messages:
30+
raise PermissionError("bot is not allowed to send messages")
731

832

9-
async def fetch_admins(bot: Bot) -> Dict:
33+
async def fetch_admins(
34+
bot: Bot,
35+
main_group_id: int,
36+
) -> dict:
1037
result = {}
11-
admins: List[Union[ChatMemberOwner, ChatMemberAdministrator]]
12-
admins = await bot.get_chat_administrators(config.group_main)
38+
admins: list[ChatMemberOwner | ChatMemberAdministrator] = await bot.get_chat_administrators(main_group_id)
1339
for admin in admins:
40+
if admin.user.is_bot:
41+
continue
1442
if isinstance(admin, ChatMemberOwner):
1543
result[admin.user.id] = {"can_restrict_members": True}
1644
else:
1745
result[admin.user.id] = {"can_restrict_members": admin.can_restrict_members}
1846
return result
1947

2048

21-
async def check_rights_and_permissions(bot: Bot, chat_id: int):
22-
chat_member_info = await bot.get_chat_member(chat_id=chat_id, user_id=bot.id)
23-
if not isinstance(chat_member_info, ChatMemberAdministrator):
24-
raise PermissionError("Bot is not an administrator")
25-
if not chat_member_info.can_restrict_members or not chat_member_info.can_delete_messages:
26-
raise PermissionError("Bot needs 'restrict participants' and 'delete messages' permissions to work properly")
27-
49+
async def set_bot_commands(bot: Bot, main_group_id: int):
50+
commands = [
51+
BotCommand(command="report", description="Report message to group admins"),
52+
]
53+
await bot.set_my_commands(commands, scope=BotCommandScopeChat(chat_id=main_group_id))

bot/callback_factories.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
from aiogram.dispatcher.filters.callback_data import CallbackData
1+
from enum import Enum
22

3+
from aiogram.filters.callback_data import CallbackData
34

4-
class DeleteMsgCallback(CallbackData, prefix="delmsg"):
5-
# action to perform: "del" to simply delete a message or "ban" to ban chat/user as well
6-
action: str
7-
# the ID of chat or user to perform action on
8-
entity_id: int
9-
# string-formatted list of message IDs to delete
10-
message_ids: str # Lists are not supported =(
5+
6+
class AdminAction(str, Enum):
7+
DELETE = "del"
8+
BAN = "ban"
9+
10+
class AdminActionCallbackV1(CallbackData, prefix="v1"):
11+
action: AdminAction
12+
user_or_chat_id: int
13+
reported_message_id: int

0 commit comments

Comments
 (0)