First commit

This commit is contained in:
2026-01-23 04:45:55 +07:00
commit 0b251c5967
118 changed files with 9580 additions and 0 deletions

2
bot/core/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .bots import *
from .webhook import *

260
bot/core/bots.py Normal file
View File

@@ -0,0 +1,260 @@
from asyncio import sleep
from datetime import datetime
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.exceptions import TelegramRetryAfter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import User, ChatAdministratorRights, BotDescription, BotShortDescription
from aiogram.utils.i18n import I18n, SimpleI18nMiddleware
from configs.config import BotSettings, BotEdit, Webhook, Permission
from middleware.loggers import log, logger
# Настройка экспорта в модули
__all__ = ("dp", "bot", "BotInfo", "i18n")
# FSM-хранилище и диспетчер
storage: MemoryStorage = MemoryStorage()
dp: Dispatcher = Dispatcher(storage=storage)
dp["is_active"]: bool = True
# Локализация
i18n: I18n = I18n(path="locales", default_locale="ru", domain="bot")
i18n_middleware: SimpleI18nMiddleware = SimpleI18nMiddleware(i18n=i18n)
i18n_middleware.setup(dp)
# Экземпляр бота
bot: Bot = Bot(
token=BotSettings.BOT_TOKEN,
default=DefaultBotProperties(
parse_mode=BotSettings.PARSE_MODE,
disable_notification=BotSettings.DISABLE_NOTIFICATION,
protect_content=BotSettings.PROTECT_CONTENT,
allow_sending_without_reply=BotSettings.ALLOW_SENDING_WITHOUT_REPLY,
link_preview_is_disabled=BotSettings.LINK_PREVIEW_IS_DISABLED,
link_preview_prefer_small_media=BotSettings.LINK_PREVIEW_PREFER_SMALL_MEDIA,
link_preview_prefer_large_media=BotSettings.LINK_PREVIEW_PREFER_LARGE_MEDIA,
link_preview_show_above_text=BotSettings.LINK_PREVIEW_SHOW_ABOVE_TEXT,
show_caption_above_media=BotSettings.SHOW_CAPTION_ABOVE_MEDIA,
),
)
class BotInfo:
"""
Класс для хранения и управления информацией о боте.
Все поля строго аннотированы, description заменено на widget.
"""
id: int | None = None
url: str | None = None
first_name: str | None = None
last_name: str | None = None
username: str | None = None
widget: str | None = None # вместо description
description: str | None = None # вместо short_description
language_code: str = BotSettings.BOT_LANGUAGE
prefix: str = BotSettings.PREFIX
bot_owner: str = BotSettings.OWNER
added_to_attachment_menu: bool = False
supports_inline_queries: bool = False
can_connect_to_business: bool = False
has_main_web_app: bool = False
can_join_groups: bool = False
can_read_all_group_messages: bool = False
rights: ChatAdministratorRights | None = None
@classmethod
@log(level="INFO", log_type="BOT", text="Настройка вебхука бота")
async def webhook(
cls, bots: Bot = bot, webhook_url: str = Webhook.WEBHOOK_URL, use_webhook: bool = Webhook.WEBHOOK
) -> None:
"""
Установка или удаление вебхука для бота.
"""
try:
await bots.delete_webhook(drop_pending_updates=True)
if use_webhook:
if webhook_url is None:
raise ValueError("Для установки вебхука необходимо указать webhook_url")
try:
await bots.set_webhook(webhook_url)
except TelegramRetryAfter as e:
logger.warning(f"Flood control при установке вебхука. Повтор через {e.retry_after} сек.")
await sleep(e.retry_after)
await bots.set_webhook(webhook_url)
except Exception as e:
logger.error(f"Ошибка при настройке вебхука: {e}")
@classmethod
@log(level="INFO", log_type="BOT", text="Получение информации о боте")
async def info(cls, bots: Bot = bot) -> dict[str, object] | None:
"""
Получает и сохраняет основные данные о боте.
"""
try:
bot_info: User = await bots.get_me()
bot_description: BotDescription = await bots.get_my_description()
bot_short_description: BotShortDescription = await bots.get_my_short_description()
bot_rights: ChatAdministratorRights = await bot.get_my_default_administrator_rights()
cls.id = bot_info.id
cls.url = f"tg://user?id={cls.id}"
cls.first_name = bot_info.first_name
cls.last_name = bot_info.last_name
cls.username = bot_info.username
cls.language_code = bot_info.language_code
# Описание (widget) и короткое описание (description)
cls.widget = bot_description.description or ""
cls.description = bot_short_description.short_description or ""
cls.added_to_attachment_menu = getattr(bot_info, "added_to_attachment_menu", False)
cls.supports_inline_queries = getattr(bot_info, "supports_inline_queries", False)
cls.can_connect_to_business = getattr(bot_info, "can_connect_to_business", False)
cls.has_main_web_app = getattr(bot_info, "has_main_web_app", False)
cls.can_join_groups = getattr(bot_info, "can_join_groups", False)
cls.can_read_all_group_messages = getattr(bot_info, "can_read_all_group_messages", False)
cls.rights = bot_rights or None
return {
"id": cls.id,
"url": cls.url,
"first_name": cls.first_name,
"last_name": cls.last_name,
"username": cls.username,
"language_code": cls.language_code,
"widget": cls.widget,
"description": cls.description,
"added_to_attachment_menu": cls.added_to_attachment_menu,
"supports_inline_queries": cls.supports_inline_queries,
"can_connect_to_business": cls.can_connect_to_business,
"has_main_web_app": cls.has_main_web_app,
"can_join_groups": cls.can_join_groups,
"can_read_all_group_messages": cls.can_read_all_group_messages,
"prefix": cls.prefix,
"bot_owner": cls.bot_owner,
"rights": cls.rights,
}
except Exception as e:
logger.error(f"Ошибка при получении информации о боте: {e}")
return None
@staticmethod
@log(level="INFO", log_type="BOT", text="Установка прав администратора")
async def set_administrator_rights(rights: ChatAdministratorRights = BotEdit.RIGHTS, bots: Bot = bot) -> None:
"""
Устанавливает дефолтные права администратора для бота.
"""
try:
current_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
if current_rights != rights:
await bots.set_my_default_administrator_rights(rights=rights)
await bots.set_my_default_administrator_rights(rights=rights, for_channels=True)
except Exception as e:
logger.error(f"Ошибка при установке прав администратора: {e}")
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление имени бота")
async def set_name(new_name: str = BotEdit.NAME, bots: Bot = bot) -> None:
"""
Обновляет имя бота (от 1 до 32 символов).
"""
try:
current_name: str = (await bots.get_me()).first_name
if not (1 <= len(new_name) <= 32):
raise ValueError("Имя бота должно быть от 1 до 32 символов.")
if current_name != new_name:
await bots.set_my_name(name=new_name)
except Exception as e:
logger.error(f"Ошибка при обновлении имени бота: {e}")
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление виджета бота")
async def set_widget(new_widget: str = BotEdit.DESCRIPTION, bots: Bot = bot) -> None:
"""
Обновляет описание бота (widget).
"""
try:
current_widget: BotDescription = await bots.get_my_description()
if not (0 < len(new_widget) <= 255):
raise ValueError("Виджет должен быть от 1 до 255 символов.")
if current_widget.description != new_widget:
await bots.set_my_description(description=new_widget)
except Exception as e:
logger.error(f"Ошибка при обновлении виджета бота: {e}")
@staticmethod
@log(level="INFO", log_type="BOT", text="Обновление короткого виджета бота")
async def set_short_widget(new_short: str = BotEdit.SHORT_DESCRIPTION, bots: Bot = bot) -> None:
"""
Обновляет короткое описание (short_widget).
"""
try:
current_short: BotShortDescription = await bots.get_my_short_description()
if not (0 < len(new_short) <= 120):
raise ValueError("Короткий виджет должен быть от 1 до 120 символов.")
if current_short.short_description != new_short:
await bots.set_my_short_description(short_description=new_short)
except Exception as e:
logger.error(f"Ошибка при обновлении короткого виджета бота: {e}")
@staticmethod
def start_info_out(out: bool = True) -> str | None:
"""
Формирует и выводит стартовую информацию о боте.
"""
try:
bot_time: str = f"Бот @{BotInfo.username} запущен в {datetime.now().strftime('%S:%M:%H %d-%m-%Y')}\n"
bot_name: str = f"Основное имя: {BotInfo.first_name}\n"
bot_postname: str = f" Доп. имя: {BotInfo.last_name}\n"
bot_description: str = f" Описание бота: {BotInfo.description}\n"
bot_widget: str = f" Виджет бота: {BotInfo.widget}\n"
bot_username: str = f" Юзернейм: @{BotInfo.username}\n"
bot_id: str = f" ID: {BotInfo.id}\n"
bot_can_join_groups: str = f" Может ли вступать в группы: {BotInfo.can_join_groups}\n"
bot_can_read_all_group_messages: str = f" Чтение всех сообщений: {BotInfo.can_read_all_group_messages}\n"
bot_added_to_attachment_menu: str = f" Добавлен в меню вложений: {BotInfo.added_to_attachment_menu}\n"
bot_supports_inline_queries: str = f" Поддерживает инлайн-запросы: {BotInfo.supports_inline_queries}\n"
bot_can_connect_to_business: str = f" Подключение к бизнес-аккаунтам: {BotInfo.can_connect_to_business}\n"
bot_has_main_web_app: str = f" Основное веб-приложение: {BotInfo.has_main_web_app}\n"
bot_prefixs: str = f" Префиксы команд бота: {BotInfo.prefix}\n"
bot_all_info: str = (
f"{bot_name} {bot_postname} {bot_description} {bot_widget} {bot_username} "
f"{bot_id} {bot_can_join_groups} {bot_can_read_all_group_messages} "
f"{bot_added_to_attachment_menu} {bot_supports_inline_queries} "
f"{bot_can_connect_to_business} {bot_has_main_web_app} {bot_prefixs}"
)
if out:
print(f"\033[34m{bot_all_info}\033[0m")
with open("Logs/info.log", "w", encoding="utf-8") as log_file:
log_file.write(f"{bot_time}{bot_all_info}")
with open("Logs/bot_start.log", "a", encoding="utf-8") as log_start_file:
log_start_file.write(f"{bot_time}\n")
return bot_all_info
except Exception as e:
logger.error(f"Ошибка при выводе стартовой информации: {e}")
return None
@classmethod
@log(level="INFO", log_type="START", text="Процесс запуска бота!")
async def setup(cls, perm: bool = Permission.BOT_EDIT, bots: Bot = bot) -> None:
"""
Настройка и инициализация всех параметров бота при старте.
"""
try:
await cls.webhook(bots=bots)
await cls.info(bots=bots)
if perm:
await cls.set_administrator_rights(bots=bots)
await cls.set_widget(bots=bots)
await cls.set_short_widget(bots=bots)
await cls.set_name(bots=bots)
except Exception as e:
logger.error(f"Ошибка при запуске настройки бота: {e}")

53
bot/core/webhook.py Normal file
View File

@@ -0,0 +1,53 @@
from typing import Any
from aiohttp import web
from aiogram.types import Update
from middleware.loggers import logger
from bot.core.bots import dp, bot
# Настройки экспорта в модули
__all__ = ("WebhookApp",)
class WebhookApp:
"""Приложение aiohttp для обработки webhook-запросов."""
def __init__(self, host: str = "0.0.0.0", port: int = 8080) -> None:
self.host = host
self.port = port
self.app: web.Application = web.Application()
self.app.router.add_post("/webhook", self.handle_update)
self.runner: web.AppRunner | None = None
self.site: web.TCPSite | None = None
@staticmethod
async def handle_update(request: web.Request) -> web.Response:
"""Обработчик входящих запросов от Telegram."""
try:
update_json: dict[str, Any] = await request.json()
update: Update = Update.model_validate(update_json)
await dp.feed_update(bot=bot, update=update)
except Exception as e:
logger.error(f"Ошибка обработки webhook-запроса: {e}")
return web.Response(status=500)
return web.Response(status=200)
async def start(self) -> None:
"""Асинхронный запуск aiohttp-приложения."""
self.runner = web.AppRunner(self.app)
await self.runner.setup()
self.site = web.TCPSite(self.runner, self.host, self.port)
await self.site.start()
logger.info(f"🌍 Webhook сервер запущен на http://{self.host}:{self.port}")
async def stop(self) -> None:
"""Остановка aiohttp-приложения."""
if self.runner:
await self.runner.cleanup()