428th-exchange-bot/bot/main.py
Artemii Peretiachenko 8f6d2c07d6 Initial commit: 428th exchange Telegram bot.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 18:19:53 +02:00

156 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import asyncio
import logging
import sys
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ChatMemberStatus, ChatType, ParseMode
from aiogram.exceptions import TelegramAPIError
from aiogram.types import ChatMemberUpdated
from bot.config import Config, apply_text_overrides, load_config
from bot.db.repository import ClientRepository
from bot.handlers.client import router as client_router
from bot.handlers.manager import build_manager_router
from bot.services.cooldown import CooldownService
from bot.services.relay import RelayService
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
stream=sys.stdout,
)
logger = logging.getLogger(__name__)
async def on_bot_added(event: ChatMemberUpdated, config: Config, relay: RelayService) -> None:
"""Log chat id when the bot is added to a group — helps set MANAGER_GROUP_ID."""
new = event.new_chat_member
if not relay.is_self(new.user.id):
return
if new.status in {ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.MEMBER}:
logger.info(
"Bot added to chat %s (%s). Use this as MANAGER_GROUP_ID if it is the forum.",
event.chat.id,
event.chat.title,
)
async def validate_manager_group(bot: Bot, manager_group_id: int) -> None:
"""Fail fast if MANAGER_GROUP_ID is wrong or the bot lacks required rights."""
try:
chat = await bot.get_chat(manager_group_id)
except TelegramAPIError as exc:
raise RuntimeError(
f"Не удалось получить чат MANAGER_GROUP_ID={manager_group_id}: {exc}. "
"Проверьте id и что бот добавлен в группу."
) from exc
if chat.type != ChatType.SUPERGROUP:
raise RuntimeError(
f"MANAGER_GROUP_ID должен быть супергруппой, сейчас type={chat.type!r} "
f"(chat_id={chat.id}, title={chat.title!r})."
)
if not bool(getattr(chat, "is_forum", False)):
raise RuntimeError(
f"В чате {chat.id} ({chat.title!r}) не включены Topics. "
"Включите темы в настройках группы."
)
me = await bot.get_me()
try:
member = await bot.get_chat_member(manager_group_id, me.id)
except TelegramAPIError as exc:
raise RuntimeError(
f"Не удалось проверить права бота в группе {chat.id}: {exc}."
) from exc
if member.status == ChatMemberStatus.CREATOR:
logger.info("Manager group OK: %s (%s), bot is creator", chat.id, chat.title)
return
if member.status != ChatMemberStatus.ADMINISTRATOR:
raise RuntimeError(
f"Бот должен быть администратором группы {chat.title!r} (id={chat.id}). "
f"Текущий статус: {member.status!r}."
)
missing: list[str] = []
if not bool(getattr(member, "can_manage_topics", True)):
missing.append("Manage topics")
if not bool(getattr(member, "can_delete_messages", True)):
missing.append("Delete messages")
can_post = getattr(member, "can_post_messages", None)
# In groups can_post_messages may be None; treat explicit False as missing.
if can_post is False:
missing.append("Post messages")
if missing:
raise RuntimeError(
f"Бот — админ группы {chat.title!r}, но не хватает прав: {', '.join(missing)}. "
"Выдайте их в настройках администраторов."
)
logger.info("Manager group OK: %s (%s), bot is admin", chat.id, chat.title)
async def run() -> None:
config = load_config()
apply_text_overrides(config)
repo = ClientRepository(config.database_path)
await repo.connect()
relay = RelayService(repo, config.manager_group_id)
cooldown = CooldownService(config.client_cooldown_seconds)
bot = Bot(
token=config.bot_token,
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)
me = await bot.get_me()
relay.set_bot_id(me.id)
try:
await validate_manager_group(bot, config.manager_group_id)
except RuntimeError as exc:
logger.error("%s", exc)
print(f"ERROR: {exc}", file=sys.stderr)
await repo.close()
await bot.session.close()
raise SystemExit(1) from exc
dp = Dispatcher()
dp["config"] = config
dp["repo"] = repo
dp["relay"] = relay
dp["cooldown"] = cooldown
dp.include_router(client_router)
dp.include_router(build_manager_router(config.manager_group_id))
dp.my_chat_member.register(on_bot_added)
logger.info(
"Starting bot @%s (manager group %s, client cooldown %.1fs)",
me.username,
config.manager_group_id,
config.client_cooldown_seconds,
)
try:
await dp.start_polling(
bot,
allowed_updates=dp.resolve_used_update_types(),
)
finally:
await repo.close()
await bot.session.close()
def main() -> None:
asyncio.run(run())
if __name__ == "__main__":
main()