типо да
This commit is contained in:
3
bot/__init__.py
Normal file
3
bot/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .core import *
|
||||
from .handlers import *
|
||||
from .middlewares import *
|
||||
2
bot/core/__init__.py
Normal file
2
bot/core/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .bots import *
|
||||
from .webhook import *
|
||||
203
bot/core/bots.py
Normal file
203
bot/core/bots.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from datetime import datetime
|
||||
|
||||
from asyncio import sleep
|
||||
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
|
||||
|
||||
__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:
|
||||
"""Класс для хранения и настройки информации о боте."""
|
||||
|
||||
id: int | None = None
|
||||
url: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
username: str | None = None
|
||||
description: str | None = None
|
||||
short_description: str | None = None
|
||||
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
|
||||
|
||||
@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:
|
||||
"""
|
||||
Удаление или установка вебхука.
|
||||
"""
|
||||
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.WEBHOOK_URL)
|
||||
except TelegramRetryAfter as e:
|
||||
print(f"Flood control: повтор через {e.retry_after} секунд")
|
||||
await sleep(e.retry_after)
|
||||
await bots.set_webhook(Webhook.WEBHOOK_URL)
|
||||
|
||||
@classmethod
|
||||
@log(level="INFO", log_type="BOT", text="Получение информации о боте")
|
||||
async def info(cls, bots: Bot = bot) -> dict[str, object]:
|
||||
"""
|
||||
Получает и сохраняет информацию о боте.
|
||||
"""
|
||||
bot_info: User = await bots.get_me()
|
||||
|
||||
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
|
||||
cls.is_premium = getattr(bot_info, "is_premium", False)
|
||||
cls.added_to_attachment_menu = bot_info.added_to_attachment_menu
|
||||
cls.supports_inline_queries = bot_info.supports_inline_queries
|
||||
cls.can_connect_to_business = bot_info.can_connect_to_business
|
||||
cls.has_main_web_app = bot_info.has_main_web_app
|
||||
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)
|
||||
|
||||
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,
|
||||
"prefix": cls.prefix,
|
||||
"bot_owner": cls.bot_owner,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Установка прав администратора")
|
||||
async def set_administrator_rights(
|
||||
bots: Bot = bot, rights: ChatAdministratorRights = BotEdit.RIGHTS
|
||||
) -> None:
|
||||
current_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
|
||||
if current_rights != rights:
|
||||
await bots.set_my_default_administrator_rights(rights)
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Обновление имени бота")
|
||||
async def set_name(bots: Bot = bot, new_name: str = BotEdit.NAME) -> None:
|
||||
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(new_name)
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Обновление описания бота")
|
||||
async def set_description(bots: Bot = bot, new_description: str = BotEdit.DESCRIPTION) -> None:
|
||||
current_description: BotDescription = await bots.get_my_description()
|
||||
if not (0 < len(new_description) <= 255):
|
||||
raise ValueError("Описание должно быть от 1 до 255 символов.")
|
||||
if current_description.description != new_description:
|
||||
await bots.set_my_description(description=new_description)
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Обновление короткого описания бота")
|
||||
async def set_short_description(bots: Bot = bot, new_short: str = BotEdit.SHORT_DESCRIPTION) -> None:
|
||||
current_short: BotShortDescription = await bots.get_my_short_description()
|
||||
if not (0 < len(new_short) <= 512):
|
||||
raise ValueError("Короткое описание должно быть от 1 до 512 символов.")
|
||||
if current_short.short_description != new_short:
|
||||
await bots.set_my_short_description(short_description=new_short)
|
||||
|
||||
@staticmethod
|
||||
def start_info_out(out: bool = True) -> str:
|
||||
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_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_all_info: str = (f"{bot_name} {bot_postname} {bot_username} {bot_id} "
|
||||
f"{bot_can_join_groups} {bot_can_read_all_group_messages} "
|
||||
f"{bot_added_to_attachment_menu} {bot_supports_inline_queries} {bot_can_connect_to_business} "
|
||||
f"{bot_has_main_web_app}")
|
||||
|
||||
if out:
|
||||
print(f"\033[34m{bot_all_info}\033[0m")
|
||||
|
||||
# Записываем информацию в файл
|
||||
try:
|
||||
with open("Logs/info.log", 'w', encoding='utf-8') as log_file:
|
||||
log_file.write(f"{bot_time}{bot_all_info}")
|
||||
|
||||
# Создание файла bot_start.log
|
||||
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:
|
||||
raise f"Ошибка при получении ID пользователя: {e}"
|
||||
|
||||
@classmethod
|
||||
@log(level="INFO", log_type="START", text="Процесс запуска бота!")
|
||||
async def setup(cls, bots: Bot = bot, perm: bool = Permission.BOT_EDIT):
|
||||
await cls.webhook(bots=bots)
|
||||
await cls.info(bots=bots)
|
||||
if perm:
|
||||
await cls.set_administrator_rights(bots=bots)
|
||||
await cls.set_description(bots=bots)
|
||||
await cls.set_short_description(bots=bots)
|
||||
await cls.set_name(bots=bots)
|
||||
44
bot/core/webhook.py
Normal file
44
bot/core/webhook.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram.types import Update
|
||||
|
||||
from .bots import dp, bot
|
||||
from middleware import loggers
|
||||
|
||||
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:
|
||||
print(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()
|
||||
loggers.info(f"🌍 Webhook сервер запущен на http://{self.host}:{self.port}")
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Остановка aiohttp-приложения."""
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
5
bot/filters/__init__.py
Normal file
5
bot/filters/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .callback import *
|
||||
from .chat_rights import *
|
||||
from .chat_type import *
|
||||
from .message_content import *
|
||||
from .subscrided import *
|
||||
21
bot/filters/callback.py
Normal file
21
bot/filters/callback.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
# Настройка экспорта
|
||||
__all__ = ("CallbackDataStartsWith",)
|
||||
|
||||
|
||||
class CallbackDataStartsWith(BaseFilter):
|
||||
"""
|
||||
Фильтр для callback_data, начинающихся с префикса.
|
||||
|
||||
Example:
|
||||
@router.callback_query(CallbackDataStartsWith("menu:"))
|
||||
async def handler(cb: CallbackQuery):
|
||||
await cb.answer("Это callback из меню ✅")
|
||||
"""
|
||||
def __init__(self, prefix: str) -> None:
|
||||
self.prefix = prefix
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> bool:
|
||||
return bool(callback.data and callback.data.startswith(self.prefix))
|
||||
73
bot/filters/chat_rights.py
Normal file
73
bot/filters/chat_rights.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from aiogram import Bot
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, ResultChatMemberUnion
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
|
||||
# Настройка экспорта
|
||||
__all__ = ("IsChatCreator", "IsAdmin", "IsModerator",)
|
||||
|
||||
|
||||
class IsChatCreator(BaseFilter):
|
||||
"""
|
||||
Пользователь является создателем чата.
|
||||
|
||||
Example:
|
||||
@router.message(IsChatCreator())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Ты создатель этого чата 👑")
|
||||
"""
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
try:
|
||||
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
return member.status == "creator"
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
return False
|
||||
|
||||
|
||||
class IsAdmin(BaseFilter):
|
||||
"""
|
||||
Пользователь является администратором (или создателем).
|
||||
|
||||
Example:
|
||||
@router.message(IsAdmin())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Ты админ ✅")
|
||||
"""
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
try:
|
||||
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
return member.status in {"administrator", "creator"}
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
return False
|
||||
|
||||
|
||||
class IsModerator(BaseFilter):
|
||||
"""
|
||||
Администратор с модераторскими правами:
|
||||
- удаление сообщений
|
||||
- ограничение пользователей
|
||||
- закрепление сообщений
|
||||
|
||||
Example:
|
||||
@router.message(IsModerator())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Ты модератор ✅")
|
||||
"""
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
try:
|
||||
member: ResultChatMemberUnion = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
|
||||
if member.status == "creator":
|
||||
return True
|
||||
if member.status != "administrator":
|
||||
return False
|
||||
|
||||
required_rights: list[bool] = [
|
||||
getattr(member, "can_delete_messages", False),
|
||||
getattr(member, "can_restrict_members", False),
|
||||
getattr(member, "can_pin_messages", False),
|
||||
]
|
||||
return all(required_rights)
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
return False
|
||||
31
bot/filters/chat_type.py
Normal file
31
bot/filters/chat_type.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
# Настройка экспорта
|
||||
__all__ = ("IsPrivate", "IsGroup",)
|
||||
|
||||
|
||||
class IsPrivate(BaseFilter):
|
||||
"""
|
||||
Сообщение в личке с ботом.
|
||||
|
||||
Example:
|
||||
@router.message(IsPrivate())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Это ЛС ✅")
|
||||
"""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return message.chat.type == "private"
|
||||
|
||||
|
||||
class IsGroup(BaseFilter):
|
||||
"""
|
||||
Сообщение в группе или супергруппе.
|
||||
|
||||
Example:
|
||||
@router.message(IsGroup())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Это сообщение в группе ✅")
|
||||
"""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return message.chat.type in {"group", "supergroup"}
|
||||
67
bot/filters/message_content.py
Normal file
67
bot/filters/message_content.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message
|
||||
|
||||
# Настройка экспорта
|
||||
__all__ = ("IsReply", "IsForwarded", "HasMedia", "ContainsURL",)
|
||||
|
||||
|
||||
class IsReply(BaseFilter):
|
||||
"""
|
||||
Сообщение является ответом.
|
||||
|
||||
Example:
|
||||
@router.message(IsReply())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Это реплай ✅")
|
||||
"""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return message.reply_to_message is not None
|
||||
|
||||
|
||||
class IsForwarded(BaseFilter):
|
||||
"""
|
||||
Сообщение переслано из другого чата/от пользователя.
|
||||
|
||||
Example:
|
||||
@router.message(IsForwarded())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Это пересланное сообщение 🔄")
|
||||
"""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return (message.forward_from is not None) or (message.forward_from_chat is not None)
|
||||
|
||||
|
||||
class HasMedia(BaseFilter):
|
||||
"""
|
||||
Сообщение содержит медиа (фото, видео, документ и т.д.).
|
||||
|
||||
Example:
|
||||
@router.message(HasMedia())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Это медиа ✅")
|
||||
"""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
return any([
|
||||
message.photo,
|
||||
message.video,
|
||||
message.document,
|
||||
message.audio,
|
||||
message.voice,
|
||||
message.video_note,
|
||||
message.sticker,
|
||||
])
|
||||
|
||||
|
||||
class ContainsURL(BaseFilter):
|
||||
"""
|
||||
Сообщение содержит ссылку (http/https).
|
||||
|
||||
Example:
|
||||
@router.message(ContainsURL())
|
||||
async def handler(msg: Message):
|
||||
await msg.answer("Это сообщение с ссылкой 🔗")
|
||||
"""
|
||||
async def __call__(self, message: Message) -> bool:
|
||||
if not message.text:
|
||||
return False
|
||||
return "http://" in message.text or "https://" in message.text
|
||||
39
bot/filters/subscrided.py
Normal file
39
bot/filters/subscrided.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from aiogram.types import Message, ResultChatMemberUnion
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from typing import Union
|
||||
|
||||
# Настройки экспорта
|
||||
__all__ = ("FilterSubscribed",)
|
||||
|
||||
|
||||
class FilterSubscribed(BaseFilter):
|
||||
"""
|
||||
Фильтр для проверки подписки пользователя на один или несколько каналов.
|
||||
Поддерживает как публичные каналы (username), так и приватные (ID).
|
||||
|
||||
Пример:
|
||||
# Проверка сразу двух каналов: публичный по username и приватный по ID
|
||||
@router.message(FilterSubscribed(["@public_channel", -1001234567890]))
|
||||
async def only_subscribed(message: Message):
|
||||
await message.answer("Ты подписан и на публичный, и на приватный канал ✅")
|
||||
"""
|
||||
def __init__(self, channels: list[Union[str, int]]) -> None:
|
||||
self.channels = channels
|
||||
|
||||
async def __call__(self, message: Message, bot: Bot) -> bool:
|
||||
for channel in self.channels:
|
||||
try:
|
||||
member: ResultChatMemberUnion = await bot.get_chat_member(
|
||||
chat_id=channel,
|
||||
user_id=message.from_user.id
|
||||
)
|
||||
if member.status in ("left", "kicked"):
|
||||
return False
|
||||
|
||||
except (TelegramBadRequest, TelegramForbiddenError):
|
||||
# Канал недоступен, либо у бота нет прав
|
||||
return False
|
||||
|
||||
return True
|
||||
15
bot/handlers/__init__.py
Normal file
15
bot/handlers/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from aiogram import Router
|
||||
#from .commands import router as cmd_routers
|
||||
from .messages import router as messages_routers
|
||||
from .secret import router as secret_routers
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
#cmd_routers,
|
||||
secret_routers,
|
||||
messages_routers,
|
||||
)
|
||||
13
bot/handlers/commands/__init__.py
Normal file
13
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from aiogram import Router
|
||||
from .admins import router as admin_cmd_router
|
||||
from .users import router as users_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
admin_cmd_router,
|
||||
users_cmd_router,
|
||||
)
|
||||
11
bot/handlers/commands/admins/__init__.py
Normal file
11
bot/handlers/commands/admins/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from aiogram import Router
|
||||
from .settings_cmd import router as settings_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
settings_cmd_router,
|
||||
)
|
||||
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils.interesting_facts import interesting_fact
|
||||
from bot.core.bots import BotInfo
|
||||
from configs import COMMANDS, RpValue
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
CMD: str = "settings".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /start"""
|
||||
await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
|
||||
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
|
||||
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
|
||||
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||
|
||||
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
|
||||
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||
|
||||
Интересный факт:
|
||||
<blockquote>{fact}</blockquote>
|
||||
"""
|
||||
).format(
|
||||
url=message.from_user.url if message.from_user else "",
|
||||
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||
rp_name=RpValue.RP_NAME,
|
||||
fact=interesting_fact(),
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||
13
bot/handlers/commands/users/__init__.py
Normal file
13
bot/handlers/commands/users/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from aiogram import Router
|
||||
from .start_cmd import router as start_cmd_router
|
||||
from .active import router as active_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
start_cmd_router,
|
||||
active_cmd_router,
|
||||
)
|
||||
45
bot/handlers/commands/users/active.py
Normal file
45
bot/handlers/commands/users/active.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from bot.templates import msg_photo
|
||||
from bot.core.bots import BotInfo
|
||||
from configs import COMMANDS
|
||||
from database import db
|
||||
|
||||
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
|
||||
CMD: str = "active".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
async def active_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /active"""
|
||||
await state.clear()
|
||||
|
||||
# Получить статистику сообщений пользователя
|
||||
day, week, month, total = await db.get_message_stats(message.from_user.id)
|
||||
|
||||
print(f"За день: {day} сообщений")
|
||||
print(f"За неделю: {week} сообщений")
|
||||
print(f"За месяц: {month} сообщений")
|
||||
print(f"Всего: {total} сообщений")
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str =f"""
|
||||
За день: {day} сообщений
|
||||
За неделю: {week} сообщений
|
||||
За месяц: {month} сообщений
|
||||
Всего: {total} сообщений
|
||||
"""
|
||||
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text,)
|
||||
0
bot/handlers/commands/users/anketa_cmd.py
Normal file
0
bot/handlers/commands/users/anketa_cmd.py
Normal file
218
bot/handlers/commands/users/new_cmd.py
Normal file
218
bot/handlers/commands/users/new_cmd.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import re
|
||||
from typing import Optional, Dict, Tuple
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.keyboards.inline.decision import decision_keyboard
|
||||
from bot.states.new_states import NewStates
|
||||
from bot.templates import msg
|
||||
from middleware.loggers import log
|
||||
from configs import COMMANDS, ImportantID, RpValue
|
||||
|
||||
# Глобальная мапа для хранения связей пользователь-топик
|
||||
user_topic_map: Dict[Tuple[int, str], int] = {}
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "new"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
TOPIC_TYPE: str = "anketa"
|
||||
|
||||
TEXTS: Dict[str, Dict[str, str]] = {
|
||||
"anketa": {
|
||||
"accept": f"<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!\n\nФлуд: {RpValue.FLUD_URL}\nРолевая: {RpValue.RP_URL}",
|
||||
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async def validate_russian_text(text: str) -> bool:
|
||||
"""Проверяет текст на соответствие русским буквам, пробелам и дефисам."""
|
||||
return bool(re.fullmatch(r"[А-Яа-яЁё\s\-]+", text))
|
||||
|
||||
|
||||
# ===================== Команда /new =====================
|
||||
@router.callback_query(F.data == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||
async def new_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Начало анкеты /new.
|
||||
Отправляет пользователю сообщение с просьбой указать желаемую роль.
|
||||
"""
|
||||
await state.clear()
|
||||
await state.set_state(NewStates.role)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
text: str = _(
|
||||
"Пожалуйста, отправьте желаемую роль:\n"
|
||||
"(только русские буквы, пробелы или дефисы)"
|
||||
)
|
||||
|
||||
await msg(message=message, text=text, markup=ikb)
|
||||
|
||||
|
||||
# ===================== Обработка роли =====================
|
||||
@router.message(NewStates.role)
|
||||
async def process_role(message: Message, state: FSMContext) -> None:
|
||||
"""Обрабатывает ввод роли и запрашивает сортол."""
|
||||
if not await validate_russian_text(message.text):
|
||||
await message.reply("Ошибка: роль должна содержать только русские буквы, пробелы или дефисы.")
|
||||
return
|
||||
|
||||
await state.update_data(role=message.text.strip().title())
|
||||
await state.set_state(NewStates.sorol)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await message.reply(
|
||||
text="Теперь укажите желаемый сортол:\n(только русские буквы, пробелы или дефисы)",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
|
||||
|
||||
# ===================== Обработка сортола =====================
|
||||
@router.message(NewStates.sorol)
|
||||
async def process_sortol(message: Message, state: FSMContext) -> None:
|
||||
"""Обрабатывает ввод сортола и запрашивает кодовую фразу."""
|
||||
if not await validate_russian_text(message.text):
|
||||
await message.reply("Ошибка: сорол должен содержать только русские буквы, пробелы или дефисы.")
|
||||
return
|
||||
|
||||
await state.update_data(sortol=message.text.strip().title())
|
||||
await state.set_state(NewStates.code_phrase)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await message.reply(
|
||||
text="Теперь введите кодовую фразу из правил:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
|
||||
|
||||
# ===================== Обработка кодовой фразы =====================
|
||||
@router.message(NewStates.code_phrase)
|
||||
async def process_code_phrase(message: Message, state: FSMContext) -> None:
|
||||
"""Обрабатывает ввод кодовой фразы и показывает предпросмотр анкеты."""
|
||||
code_phrase = message.text.strip()
|
||||
if not code_phrase:
|
||||
await message.reply("Кодовая фраза не может быть пустой.")
|
||||
return
|
||||
|
||||
await state.update_data(code_phrase=code_phrase)
|
||||
data: Dict[str, str] = await state.get_data()
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(
|
||||
InlineKeyboardButton(text="Отправить!", callback_data="submit_new"),
|
||||
InlineKeyboardButton(text="Отмена↩️", callback_data="start")
|
||||
)
|
||||
|
||||
text: str = (
|
||||
f"<b>Проверьте данные анкеты:</b>\n\n"
|
||||
f"• Роль: {data['role']}\n"
|
||||
f"• Сортол: {data['sortol']}\n"
|
||||
f"• Кодовая фраза: {data['code_phrase']}"
|
||||
)
|
||||
|
||||
await message.reply(text, reply_markup=ikb.as_markup())
|
||||
|
||||
|
||||
# ===================== Отправка анкеты в поддержку =====================
|
||||
@router.callback_query(F.data == "submit_new")
|
||||
async def submit_new_cmd(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Отправляет анкету в топик форума поддержки и создает запись в мапе."""
|
||||
data: Dict[str, str] = await state.get_data()
|
||||
user = callback.from_user
|
||||
|
||||
# Создаем топик в форуме
|
||||
topic = await callback.bot.create_forum_topic(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
name=f"Анкета от {user.full_name}"
|
||||
)
|
||||
thread_id: int = topic.message_thread_id
|
||||
|
||||
# Сохраняем связь пользователь-топик
|
||||
user_topic_map[(user.id, TOPIC_TYPE)] = thread_id
|
||||
|
||||
# Формируем текст анкеты
|
||||
text: str = (
|
||||
f'<b><a href="tg://user?id={user.id}">Анкета</a></b>\n\n'
|
||||
f"• Роль: {data['role']}\n"
|
||||
f"• Сортол: {data['sortol']}\n"
|
||||
f"• Кодовая фраза: {data['code_phrase']}"
|
||||
)
|
||||
|
||||
# Отправляем в топик с кнопками принятия/отклонения
|
||||
await callback.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=decision_keyboard(thread_id=thread_id, kind=TOPIC_TYPE)
|
||||
)
|
||||
|
||||
await callback.message.edit_text("✅ Ваша анкета успешно отправлена на рассмотрение!")
|
||||
await state.clear()
|
||||
|
||||
|
||||
# ===================== Обработка решения админов =====================
|
||||
@router.callback_query(F.data.regexp(r"^([a-z_]+):(accept|reject):(\d+)$"))
|
||||
async def process_decision_callback(callback: CallbackQuery) -> None:
|
||||
"""Обрабатывает решение администраторов и отправляет результат пользователю."""
|
||||
kind, action, thread_id_str = callback.data.split(":")
|
||||
thread_id = int(thread_id_str)
|
||||
|
||||
# Ищем пользователя по thread_id в мапе
|
||||
user_id = None
|
||||
for (uid, k), tid in user_topic_map.items():
|
||||
if k == kind and tid == thread_id:
|
||||
user_id = uid
|
||||
break
|
||||
|
||||
if not user_id:
|
||||
await callback.answer("Пользователь не найден.", show_alert=True)
|
||||
return
|
||||
|
||||
text_to_send: Optional[str] = TEXTS.get(kind, {}).get(action)
|
||||
if not text_to_send:
|
||||
await callback.answer("Некорректные данные.", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.bot.send_message(chat_id=user_id, text=text_to_send, parse_mode="HTML")
|
||||
await callback.message.edit_reply_markup(reply_markup=None)
|
||||
await callback.answer("Ответ отправлен пользователю.")
|
||||
|
||||
|
||||
# ===================== Пересылка ответов админов пользователю =====================
|
||||
@router.message(F.is_topic_message, F.reply_to_message, ~F.from_user.is_bot)
|
||||
async def forward_reply_to_user(message: Message) -> None:
|
||||
"""Пересылает ответы администраторов из топика пользователю."""
|
||||
thread_id = message.message_thread_id
|
||||
if not thread_id:
|
||||
return
|
||||
|
||||
# Ищем пользователя по thread_id
|
||||
user_id = None
|
||||
for (uid, _), tid in user_topic_map.items():
|
||||
if tid == thread_id:
|
||||
user_id = uid
|
||||
break
|
||||
|
||||
if not user_id:
|
||||
return
|
||||
|
||||
reply_text: str = f"<b>Ответ администратора:</b>\n{message.html_text}"
|
||||
try:
|
||||
await message.bot.send_message(chat_id=user_id, text=reply_text, parse_mode="HTML")
|
||||
except Exception as e:
|
||||
await message.reply(f"⚠️ Не удалось отправить сообщение пользователю: {e}")
|
||||
51
bot/handlers/commands/users/start_cmd.py
Normal file
51
bot/handlers/commands/users/start_cmd.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils.interesting_facts import interesting_fact
|
||||
from bot.core.bots import BotInfo
|
||||
from configs import COMMANDS, RpValue
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
CMD: str = "start".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /start"""
|
||||
await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=RpValue.INFO_URL))
|
||||
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
|
||||
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
|
||||
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||
|
||||
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
|
||||
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||
|
||||
Интересный факт:
|
||||
<blockquote>{fact}</blockquote>
|
||||
"""
|
||||
).format(
|
||||
url=message.from_user.url if message.from_user else "",
|
||||
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||
rp_name=RpValue.RP_NAME,
|
||||
fact=interesting_fact(),
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||
15
bot/handlers/messages/__init__.py
Normal file
15
bot/handlers/messages/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from aiogram import Router
|
||||
from .default import router as default_message_router
|
||||
from .reply_msg import router as reply_message_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ('router',)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подготовка роутера команд
|
||||
#router.include_routers(
|
||||
#reply_message_router,
|
||||
#)
|
||||
|
||||
# Подключение стандартного роутера
|
||||
router.include_router(default_message_router)
|
||||
139
bot/handlers/messages/default.py
Normal file
139
bot/handlers/messages/default.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from typing import Dict, List
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.utils import get_best_response
|
||||
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name="message_router")
|
||||
|
||||
|
||||
# === Словарь ключевых слов (синонимы) и возможных ответов ===
|
||||
RESPONSES: Dict[str, Dict[str, List[str]]] = {
|
||||
"док": {
|
||||
"keywords": ["доктор", "док", "дотторе", "зандик"],
|
||||
"answers": [
|
||||
"Дотторе довольно милый друг! Мне нравится проводить с ним время!",
|
||||
"Иногда он бывает слишком суровым... Но я верю, что смогу его перевоспитать!",
|
||||
"Мне иногда кажется, что он знает больше историй, чем хранится в библиотеке!",
|
||||
"Дотторе говорит загадками... а я всё равно не всегда понимаю!",
|
||||
"Он умный, но я уверен — внутри он добрый!",
|
||||
"Дотторе иногда ворчит, но всё равно заботится обо мне по-своему!",
|
||||
"Он часто думает о науке... а я думаю о печеньках!",
|
||||
"Мне кажется, он притворяется злым, а на самом деле просто боится дружбы.",
|
||||
"Когда он работает, в комнате становится тихо... даже огонь боится мешать ему.",
|
||||
"Я иногда думаю... а улыбается ли он, когда меня не видит?",
|
||||
],
|
||||
},
|
||||
"ара": {
|
||||
"keywords": ["ара", "аранара", "аранары", "ары", "кто ты", "ты кто"],
|
||||
"answers": [
|
||||
"Мы, аранары, очень любим веселиться и смеяться!",
|
||||
"Хи-хи! 🌱 Ты можешь звать меня Ари!",
|
||||
"Наш народ живёт уже тысячи лет... но мы не умеем считать!",
|
||||
"Я маленький грибочек, но у меня большое сердце!",
|
||||
"Аранара — это хранитель улыбок и весёлых историй!",
|
||||
"Я люблю играть с детьми и рассказывать им истории!",
|
||||
"Говорят, что аранары видят то, что скрыто от других.",
|
||||
"Я — часть этой библиотеки, её дыхание и её смех!",
|
||||
"Аранара — это маленький проводник в мир грёз и чудес.",
|
||||
"Мы появляемся там, где нужен друг, даже если никто не звал!",
|
||||
],
|
||||
},
|
||||
"малыш": {
|
||||
"keywords": ["малыш", "девочка", "малышка", "она", "болезнь"],
|
||||
"answers": [
|
||||
"Она милая девочка! Жаль, что больна!",
|
||||
"Она обожает сказки! Может, именно поэтому засыпает так сладко.",
|
||||
"А как её зовут?.. Я всегда забываю спросить!",
|
||||
"Иногда во сне она улыбается... значит, ей снятся хорошие истории.",
|
||||
"Дотторе грустит, когда смотрит на неё... но я верю, он её спасёт!",
|
||||
"Она словно светильник в тёмной комнате... даже если свет её тускнеет.",
|
||||
"Я думаю, её мечты сильнее болезни.",
|
||||
],
|
||||
},
|
||||
"эфир": {
|
||||
"keywords": ["эфир", "проект", "изобретение", "сплав", "эксперимент", "ядро"],
|
||||
"answers": [
|
||||
"Эфир звучит как ветер, который нельзя поймать... но можно почувствовать!",
|
||||
"Дотторе часто говорит о проектах, но я понимаю в них только половину!",
|
||||
"Каждый новый сплав для него как новая история для меня.",
|
||||
"Эксперимент — это как игра, только иногда она пахнет гарью...",
|
||||
"Я слышал, что ядро может изменить всё... даже судьбы людей.",
|
||||
"В лаборатории так много звуков — шипение кислот, стук молотов, шёпот формул.",
|
||||
"Иногда мне кажется, что изобретения Дотторе живут своей жизнью...",
|
||||
"Эфир? Кефир? ЗЕФИР!",
|
||||
],
|
||||
},
|
||||
"мысль": {
|
||||
"keywords": ["мысл", "мысль", "мысли", "думаешь"],
|
||||
"answers": [
|
||||
"О чём я думаю?.. Иногда о печеньках!",
|
||||
"Голова как будто полная тумана...",
|
||||
"Кажется, я что-то забыл... но не могу вспомнить...",
|
||||
"Мысли приходят и уходят, как маленькие птички.",
|
||||
"А ты когда-нибудь задумывался, откуда приходят мысли?",
|
||||
"Иногда мои мысли путаются и превращаются в сказки.",
|
||||
"Я думаю, что думать — это тяжело... лучше веселиться!",
|
||||
"Может, мысли — это просто шёпот библиотеки в моей голове?",
|
||||
"Когда я думаю слишком долго — у меня начинает чесаться макушка!",
|
||||
"Мысли — как облака... смотришь, и они уже другие.",
|
||||
],
|
||||
},
|
||||
"тайн": {
|
||||
"keywords": ["тайн", "тайны", "тайну", "тайна"],
|
||||
"answers": [
|
||||
"Тайны? О-о, мы играем в детективов?!",
|
||||
"Я знаю много секретов... но не все можно рассказывать!",
|
||||
"Иногда самые большие тайны прячутся на виду.",
|
||||
"Тайна — это как закрытая книга. Ты хочешь открыть её?",
|
||||
"Хи-хи... а если твоя тайна уже записана в библиотеке?",
|
||||
"Некоторые тайны лучше хранить, чем раскрывать.",
|
||||
"Каждый друг — это тоже тайна, которую мы открываем постепенно.",
|
||||
"А твои секреты я храню надёжнее любого сундука!",
|
||||
"Тайна — это искра любопытства! Без неё жизнь скучная.",
|
||||
"Ш-ш-ш... хочешь услышать одну маленькую, но очень смешную тайну?",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
# === Случайные фразы, если совпадения нет ===
|
||||
RANDOM_PHRASES: List[str] = [
|
||||
"Я Ари! Компаньон Дотторе и ваш лучший друг! Можете обращаться ко мне!",
|
||||
"Я живу здесь уже десятки лет... и мне всё ещё весело!",
|
||||
"Кхм... почему ты так странно разговариваешь? Ничего не понимаю!",
|
||||
"Мы играем в шарады? Давай попробуй ещё раз, может я пойму хоть одно слово!",
|
||||
"Ты кажешься таким загадочным... прямо как проекты Дотторе, которые меня вечно пугают!",
|
||||
"Ой! Ты меня напугал! Но всё равно приятно видеть нового друга!",
|
||||
"Если вдруг станет грустно — просто обними аранару. Мы очень мягкие!",
|
||||
"Иногда даже мне хочется спрятаться между колб и подремать...",
|
||||
"А может, именно твоё слово станет началом новой истории?",
|
||||
"Дотторе говорит, что я слишком болтлив... а разве это плохо?",
|
||||
"Ты такой серьёзный... может, стоит немного пошутить?",
|
||||
"Иногда кажется, что слова сами выбирают нас, а не мы их!",
|
||||
]
|
||||
|
||||
|
||||
# === Хэндлеры ===
|
||||
@router.message()
|
||||
async def handle_message(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Обрабатывает входящие сообщения от пользователя.
|
||||
Определяет ответ по ключевым словам или случайную фразу.
|
||||
|
||||
:param message: объект сообщения
|
||||
:param state: FSMContext для работы с состояниями
|
||||
"""
|
||||
await state.clear()
|
||||
|
||||
response: str = get_best_response(
|
||||
message.text or "",
|
||||
responses=RESPONSES,
|
||||
random_phrases=RANDOM_PHRASES,
|
||||
)
|
||||
|
||||
await message.answer(text=response)
|
||||
39
bot/handlers/messages/reply_msg.py
Normal file
39
bot/handlers/messages/reply_msg.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from random import choice
|
||||
from typing import List
|
||||
from aiogram import Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
router: Router = Router(name="reply_router")
|
||||
|
||||
RANDOM_PHRASES: List[str] = [
|
||||
"Бла-бла-бла!", "Хва-а-а-тит!", "Серьёзно? 😏", "Опять ты это говоришь...",
|
||||
"Хи-хи, смешно же!", "Ты снова шутник?", "Я уже слышал это раньше!", "Эй, не надо так!",
|
||||
"Ладно, ладно, хватит!", "Хмм... интересно...", "Ты меня удивляешь!", "А давай лучше что-то новое?",
|
||||
"Не могу поверить!", "Ахаха, это забавно!", "Серьёзно? Ну ладно...", "Эй, это уже слишком!",
|
||||
"О, это было неожиданно!",
|
||||
]
|
||||
|
||||
|
||||
@router.message()
|
||||
async def reply_message(message: Message, state: FSMContext) -> None:
|
||||
# Достаём данные из состояния
|
||||
data = await state.get_data()
|
||||
last_bot_text = data.get("last_bot_text", "")
|
||||
|
||||
# КРИТИЧЕСКИ ВАЖНО: Проверяем, что состояние не пустое после перезапуска.
|
||||
# Если состояние пустое (например, после перезапуска), то мы НЕ должны считать,
|
||||
if last_bot_text and message.text and message.text.strip() == last_bot_text.strip():
|
||||
response = "Не повторяй за мной!"
|
||||
else:
|
||||
response = choice(RANDOM_PHRASES)
|
||||
|
||||
ids = message.message_id-1
|
||||
print(str())
|
||||
|
||||
# Отправляем ответ и ПОЛУЧАЕМ ОБЪЕКТ ОТПРАВЛЕННОГО СООБЩЕНИЯ
|
||||
sent_message = await message.reply(response)
|
||||
|
||||
# Сохраняем текст последнего сообщения бота в состоянии
|
||||
# Теперь состояние будет обновлено после каждого сообщения бота
|
||||
await state.update_data(last_bot_text=sent_message.text)
|
||||
13
bot/handlers/secret/__init__.py
Normal file
13
bot/handlers/secret/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from aiogram import Router
|
||||
from .secret1 import router as secret1_router
|
||||
#from .secret2 import router as secret2_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ('router',)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение секретного роутера
|
||||
router.include_routers(
|
||||
secret1_router,
|
||||
#secret2_router,
|
||||
)
|
||||
45
bot/handlers/secret/secret1.py
Normal file
45
bot/handlers/secret/secret1.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
from aiogram.utils.markdown import hide_link
|
||||
|
||||
from middleware.loggers import log
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name="secret_router")
|
||||
CMD: str = "secret_1"
|
||||
|
||||
|
||||
@router.message(F.text.lower() == "истинная цель короля всегда было мироздание")
|
||||
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||
async def secret1_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""Обработчик секретов"""
|
||||
await state.clear()
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = f"""{hide_link("https://rp.primo.dpdns.org/wp-content/uploads/2025/08/1234567.png")}
|
||||
<b><u>Запись №-...18</u></b>
|
||||
|
||||
<blockquote><i>Значит, этот </i><i><b>правда</b> действительно </i><i><b>существует…</b> Хах.. Хахахах! Я смог найти решение</i><i><b>! Я!! СМОГ!!!</b>
|
||||
Все линии, пропорции, каждый слой металла и кристалла — всё сходится.
|
||||
Сколько лет я </i><i><b>скитался</b> по лабораториям, библиотекам, ища этот след… а ведь всё это время </i><i><b>ключ</b> к моей цели лежал прямо перед глазами — на чертеже.</i></blockquote>
|
||||
|
||||
<blockquote><i>Получится ли у меня...?\n
|
||||
Я создаю не просто броню. Я пытаюсь воплотить в материале замысел, который перевернёт всё, что мы знали.
|
||||
Каждый слой, каждая руна — это шаг к воплощению моей идеи. Даже спустя десятки лет я помню, как возвращал из небытия те конструкции, что раньше казались невозможными…</i></blockquote>
|
||||
|
||||
<blockquote><i>Возможно ли, что сама структура материи и магии </i><i><b>изменит..</b>
|
||||
Или же это моя броня станет первым устройством, способное изменить их мнение?..</i></blockquote>
|
||||
|
||||
<blockquote><i>И всё же один вопрос не даёт мне покоя: сможет ли этот замысел завершить то, что я задумал…
|
||||
Станет ли моя броня инструментом, с помощью которого замысел воплотится в реальность?..</i></blockquote>
|
||||
|
||||
<blockquote><i>Пожалуй, придётся ещё раз вернуться к чертежам и проверить расчёты.
|
||||
Что-то подсказывает мне: каждая линия, каждый символ на этом листе — это не просто металл и руны, это путь к моей великой… </i><i><b>идее</b>. ~</i></blockquote>
|
||||
|
||||
<tg-spoiler>Да… это оно. Всё ведёт к замыслу, к который я стремился десятилетиями…</tg-spoiler>"""
|
||||
|
||||
|
||||
# Отправляем сообщение
|
||||
await message.reply(text=text)
|
||||
134
bot/handlers/secret/secret2.py
Normal file
134
bot/handlers/secret/secret2.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import Message
|
||||
|
||||
# Создаем роутер
|
||||
knowledge_router = Router()
|
||||
|
||||
|
||||
# Определяем состояния
|
||||
class KnowledgeStates(StatesGroup):
|
||||
question1 = State()
|
||||
question2 = State()
|
||||
question3 = State()
|
||||
question4 = State()
|
||||
question5 = State()
|
||||
question6 = State()
|
||||
|
||||
|
||||
# Вопросы и ответы (замените на свои)
|
||||
QUESTIONS = {
|
||||
1: "Вопрос1",
|
||||
2: "Вопрос2",
|
||||
3: "Вопрос3",
|
||||
4: "Вопрос4",
|
||||
5: "Вопрос5",
|
||||
6: "Вопрос6"
|
||||
}
|
||||
|
||||
ANSWERS = {
|
||||
1: {"Ответ 11": "СообщениеА1", "Ответ 12": "СообщениеБ1"},
|
||||
2: {"Ответ 21": "СообщениеА2", "Ответ 22": "СообщениеБ2"},
|
||||
3: {"Ответ 31": "СообщениеА3", "Ответ 32": "СообщениеБ3"},
|
||||
4: {"Ответ 41": "СообщениеА4", "Ответ 42": "СообщениеБ4"},
|
||||
5: {"Ответ 51": "СообщениеА5", "Ответ 52": "СообщениеБ5"},
|
||||
6: {"Ответ 61": "СообщениеА6", "Ответ 62": "СообщениеБ6"}
|
||||
}
|
||||
|
||||
FINAL_MESSAGES = {
|
||||
"all_1": "ИТОГ1 - Все ответы первого типа!",
|
||||
"all_2": "ИТОГ2 - Все ответы второго типа!",
|
||||
"mixed": "ИТОГ1 - Смешанные ответы!"
|
||||
}
|
||||
|
||||
|
||||
# Запуск сессии знаний
|
||||
@knowledge_router.message(StateFilter(None), Command("знания"))
|
||||
@knowledge_router.message(StateFilter(None), F.text.casefold() == "пора заняться знаниями")
|
||||
async def start_knowledge_session(message: Message, state: FSMContext):
|
||||
await message.answer("Отлично! Начинаем сессию знаний! 🧠")
|
||||
await message.answer(QUESTIONS[1])
|
||||
await state.set_state(KnowledgeStates.question1)
|
||||
await state.update_data(answers={})
|
||||
|
||||
|
||||
# Обработчики для каждого вопроса
|
||||
@knowledge_router.message(KnowledgeStates.question1, F.text.in_(ANSWERS[1].keys()))
|
||||
async def process_question1(message: Message, state: FSMContext):
|
||||
user_answer = message.text
|
||||
response_message = ANSWERS[1][user_answer]
|
||||
|
||||
# Сохраняем ответ
|
||||
answer_code = 1 if user_answer == "Ответ 11" else 2
|
||||
await state.update_data(answers={"q1": answer_code})
|
||||
|
||||
# Отправляем сообщение и следующий вопрос
|
||||
await message.answer(response_message + "\n\n" + QUESTIONS[2])
|
||||
await state.set_state(KnowledgeStates.question2)
|
||||
|
||||
|
||||
@knowledge_router.message(KnowledgeStates.question2, F.text.in_(ANSWERS[2].keys()))
|
||||
async def process_question2(message: Message, state: FSMContext):
|
||||
user_answer = message.text
|
||||
response_message = ANSWERS[2][user_answer]
|
||||
|
||||
# Сохраняем ответ
|
||||
answer_code = 1 if user_answer == "Ответ 21" else 2
|
||||
data = await state.get_data()
|
||||
answers = data.get("answers", {})
|
||||
answers["q2"] = answer_code
|
||||
await state.update_data(answers=answers)
|
||||
|
||||
# Отправляем сообщение и следующий вопрос
|
||||
await message.answer(response_message + "\n\n" + QUESTIONS[3])
|
||||
await state.set_state(KnowledgeStates.question3)
|
||||
|
||||
|
||||
# Добавьте аналогичные обработчики для question3-question5
|
||||
|
||||
@knowledge_router.message(KnowledgeStates.question6, F.text.in_(ANSWERS[6].keys()))
|
||||
async def process_question6(message: Message, state: FSMContext):
|
||||
user_answer = message.text
|
||||
response_message = ANSWERS[6][user_answer]
|
||||
|
||||
# Сохраняем ответ
|
||||
answer_code = 1 if user_answer == "Ответ 61" else 2
|
||||
data = await state.get_data()
|
||||
answers = data.get("answers", {})
|
||||
answers["q6"] = answer_code
|
||||
await state.update_data(answers=answers)
|
||||
|
||||
# Отправляем финальное сообщение
|
||||
await message.answer(response_message)
|
||||
await finish_knowledge_session(message, state)
|
||||
|
||||
|
||||
# Обработчики для некорректных ответов
|
||||
@knowledge_router.message(KnowledgeStates.question1)
|
||||
async def process_incorrect_answer1(message: Message):
|
||||
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
|
||||
await message.answer(QUESTIONS[1])
|
||||
|
||||
|
||||
@knowledge_router.message(KnowledgeStates.question2)
|
||||
async def process_incorrect_answer2(message: Message):
|
||||
await message.answer("Пожалуйста, выберите один из предложенных вариантов ответа.")
|
||||
await message.answer(QUESTIONS[2])
|
||||
|
||||
|
||||
# Добавьте аналогичные обработчики для остальных вопросов
|
||||
|
||||
# Завершение сессии
|
||||
async def finish_knowledge_session(message: Message, state: FSMContext):
|
||||
data = await state.get_data()
|
||||
answers = data.get("answers", {})
|
||||
|
||||
# Проверяем результаты
|
||||
if all(answer == 2 for answer in answers.values()):
|
||||
await message.answer(FINAL_MESSAGES["all_2"])
|
||||
else:
|
||||
await message.answer(FINAL_MESSAGES["mixed"])
|
||||
|
||||
await state.clear()
|
||||
2
bot/keyboards/__init__.py
Normal file
2
bot/keyboards/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .reply import *
|
||||
from .inline import *
|
||||
1
bot/keyboards/inline/__init__.py
Normal file
1
bot/keyboards/inline/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .decision import *
|
||||
17
bot/keyboards/inline/decision.py
Normal file
17
bot/keyboards/inline/decision.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
def decision_keyboard(thread_id: int, kind: str) -> InlineKeyboardMarkup:
|
||||
"""
|
||||
Получение клавиатуры Принятия\Отклонить.
|
||||
|
||||
:param thread_id: Айди действия.
|
||||
:param kind: Вид для клавиатуры.
|
||||
:return: Инлайн-клавиатуру (Принять, Отклонить).
|
||||
"""
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(
|
||||
InlineKeyboardButton(text="✅ Принять", callback_data=f"{kind}:accept:{thread_id}"),
|
||||
InlineKeyboardButton(text="❌ Отклонить", callback_data=f"{kind}:reject:{thread_id}")
|
||||
)
|
||||
return ikb.as_markup()
|
||||
0
bot/keyboards/reply/__init__.py
Normal file
0
bot/keyboards/reply/__init__.py
Normal file
48
bot/middlewares/__init__.py
Normal file
48
bot/middlewares/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from aiogram import Dispatcher, Bot
|
||||
|
||||
from configs import ImportantID
|
||||
from .logging_mdw import LoggingMiddleware
|
||||
from .msg_mdw import MessageCounterMiddleware
|
||||
from .spam_mdw import RateLimitMiddleware
|
||||
from .subscription_mdw import SubscriptionMiddleware
|
||||
from .error_mdw import ErrorHandlingMiddleware
|
||||
from .time_mdw import TimingMiddleware
|
||||
|
||||
# Настройки экспорта
|
||||
__all__ = (
|
||||
"LoggingMiddleware",
|
||||
"SubscriptionMiddleware",
|
||||
"RateLimitMiddleware",
|
||||
"ErrorHandlingMiddleware",
|
||||
"TimingMiddleware",
|
||||
"MessageCounterMiddleware",
|
||||
"setup_middlewares",)
|
||||
|
||||
|
||||
def setup_middlewares(dp: Dispatcher, bot: Bot, channel_ids: list[int | str] = None) -> None:
|
||||
"""
|
||||
Регистрирует все middleware в диспетчере.
|
||||
"""
|
||||
channel_ids: list = channel_ids or []
|
||||
|
||||
# Middleware для ВСЕХ событий (update level)
|
||||
middlewares_updates: list = [
|
||||
TimingMiddleware(), # Замер времени
|
||||
LoggingMiddleware(), # Логирование
|
||||
ErrorHandlingMiddleware(admin_ids=ImportantID.ADMIN_ID), # Обработка ошибок
|
||||
]
|
||||
|
||||
# Middleware только для СООБЩЕНИЙ (message level)
|
||||
middlewares_msg: list = [
|
||||
#RateLimitMiddleware(rate_limit=3, time_period=5.0), # Антифлуд
|
||||
#SubscriptionMiddleware(bot=bot, channel_ids=channel_ids), # Проверка подписки
|
||||
MessageCounterMiddleware(), # Подсчет сообщений
|
||||
]
|
||||
|
||||
# Регистрируем middleware для всех событий
|
||||
for middleware in middlewares_updates:
|
||||
dp.update.middleware(middleware)
|
||||
|
||||
# Регистрируем middleware только для сообщений
|
||||
for middleware in middlewares_msg:
|
||||
dp.message.middleware(middleware)
|
||||
201
bot/middlewares/error_mdw.py
Normal file
201
bot/middlewares/error_mdw.py
Normal file
@@ -0,0 +1,201 @@
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
|
||||
|
||||
from middleware.loggers import loggers # ваш логгер
|
||||
|
||||
|
||||
class ErrorHandlingMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для глобальной обработки ошибок в хендлерах.
|
||||
|
||||
Зачем нужен:
|
||||
- Централизованная обработка исключений
|
||||
- Уведомление администраторов об ошибках
|
||||
- Graceful degradation при сбоях
|
||||
"""
|
||||
|
||||
def __init__(self, admin_ids: list[int]):
|
||||
"""
|
||||
Инициализация middleware обработки ошибок.
|
||||
|
||||
Args:
|
||||
admin_ids: Список ID администраторов для уведомлений
|
||||
"""
|
||||
self.admin_ids = admin_ids
|
||||
super().__init__()
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Перехватывает и обрабатывает ошибки в хендлерах.
|
||||
"""
|
||||
try:
|
||||
return await handler(event, data)
|
||||
|
||||
except Exception as e:
|
||||
# Получаем информацию о пользователе безопасным способом
|
||||
user_str = self._extract_user_info(event)
|
||||
|
||||
# Логируем ошибку
|
||||
error_message = f"Ошибка в хендлере: {type(e).__name__}: {str(e)}"
|
||||
|
||||
loggers.error(
|
||||
text=error_message,
|
||||
log_type="HANDLER_ERROR",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Уведомляем администраторов
|
||||
await self._notify_admins(error_message, event, user_str)
|
||||
|
||||
# Отправляем пользователю сообщение об ошибке
|
||||
await self._send_error_message(event, user_str)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_user_info(event: TelegramObject) -> str:
|
||||
"""
|
||||
Безопасно извлекает информацию о пользователе из события.
|
||||
|
||||
Args:
|
||||
event: Объект события
|
||||
|
||||
Returns:
|
||||
Строка с идентификатором пользователя
|
||||
"""
|
||||
user_str = "@System"
|
||||
|
||||
# Для Message и CallbackQuery
|
||||
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
|
||||
user = event.from_user
|
||||
user_str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
# Для Update (который содержит message или callback_query)
|
||||
elif isinstance(event, Update):
|
||||
# Пытаемся найти пользователя в различных полях Update
|
||||
user_object = None
|
||||
if event.message and event.message.from_user:
|
||||
user_object = event.message.from_user
|
||||
elif event.edited_message and event.edited_message.from_user:
|
||||
user_object = event.edited_message.from_user
|
||||
elif event.callback_query and event.callback_query.from_user:
|
||||
user_object = event.callback_query.from_user
|
||||
elif event.channel_post and event.channel_post.from_user:
|
||||
user_object = event.channel_post.from_user
|
||||
elif event.edited_channel_post and event.edited_channel_post.from_user:
|
||||
user_object = event.edited_channel_post.from_user
|
||||
|
||||
if user_object:
|
||||
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
|
||||
|
||||
return user_str
|
||||
|
||||
@staticmethod
|
||||
def _extract_event_text(event: TelegramObject) -> str:
|
||||
"""
|
||||
Безопасно извлекает текст из события.
|
||||
|
||||
Args:
|
||||
event: Объект события
|
||||
|
||||
Returns:
|
||||
Текст события или пустая строка
|
||||
"""
|
||||
event_text = ""
|
||||
|
||||
# Для Message
|
||||
if isinstance(event, Message) and hasattr(event, 'text') and event.text:
|
||||
event_text = event.text
|
||||
# Для CallbackQuery
|
||||
elif isinstance(event, CallbackQuery) and hasattr(event, 'data') and event.data:
|
||||
event_text = f"callback: {event.data}"
|
||||
# Для Update
|
||||
elif isinstance(event, Update):
|
||||
if event.message and event.message.text:
|
||||
event_text = event.message.text
|
||||
elif event.callback_query and event.callback_query.data:
|
||||
event_text = f"callback: {event.callback_query.data}"
|
||||
elif event.edited_message and event.edited_message.text:
|
||||
event_text = event.edited_message.text
|
||||
|
||||
return event_text[:100] + "..." if len(event_text) > 100 else event_text
|
||||
|
||||
async def _notify_admins(
|
||||
self,
|
||||
error_message: str,
|
||||
event: TelegramObject,
|
||||
user_str: str
|
||||
) -> None:
|
||||
"""Уведомляет администраторов об ошибке."""
|
||||
from aiogram import Bot
|
||||
bot: Bot = event.bot if hasattr(event, 'bot') else None
|
||||
|
||||
if bot:
|
||||
for admin_id in self.admin_ids:
|
||||
try:
|
||||
event_info = f"Событие: {type(event).__name__}"
|
||||
event_text = self._extract_event_text(event)
|
||||
if event_text:
|
||||
event_info += f", текст: {event_text}"
|
||||
|
||||
full_message = (
|
||||
f"🚨 Ошибка в боте:\n\n"
|
||||
f"Пользователь: {user_str}\n"
|
||||
f"Ошибка: {error_message}\n"
|
||||
f"{event_info}"
|
||||
)
|
||||
|
||||
await bot.send_message(admin_id, full_message)
|
||||
|
||||
loggers.info(
|
||||
text=f"Администратор {admin_id} уведомлен об ошибке",
|
||||
log_type="ADMIN_NOTIFIED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
loggers.error(
|
||||
text=f"Не удалось уведомить админа {admin_id}: {e}",
|
||||
log_type="ADMIN_NOTIFY_ERROR",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _send_error_message(
|
||||
event: TelegramObject,
|
||||
user_str: str
|
||||
) -> None:
|
||||
"""Отправляет пользователю сообщение об ошибке."""
|
||||
error_text = (
|
||||
"⚠️ Произошла непредвиденная ошибка. "
|
||||
"Разработчики уже уведомлены и работают над исправлением.\n\n"
|
||||
"Попробуйте повторить действие позже или нажмите /start"
|
||||
)
|
||||
|
||||
try:
|
||||
if isinstance(event, Message):
|
||||
await event.answer(error_text)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.message.answer(error_text)
|
||||
await event.answer()
|
||||
elif isinstance(event, Update) and event.message:
|
||||
await event.message.answer(error_text)
|
||||
|
||||
loggers.info(
|
||||
text="Пользователю отправлено сообщение об ошибке",
|
||||
log_type="ERROR_MESSAGE_SENT",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
loggers.error(
|
||||
text=f"Не удалось отправить сообщение об ошибке: {e}",
|
||||
log_type="ERROR_MESSAGE_FAILED",
|
||||
user=user_str
|
||||
)
|
||||
271
bot/middlewares/logging_mdw.py
Normal file
271
bot/middlewares/logging_mdw.py
Normal file
@@ -0,0 +1,271 @@
|
||||
from typing import Callable, Awaitable, Any, Dict, Optional, Tuple, Set
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Update, Message, CallbackQuery, MaybeInaccessibleMessageUnion, User
|
||||
|
||||
from bot.utils import type_msg
|
||||
from middleware.loggers import loggers # ваш глобальный логгер
|
||||
from configs import BotSettings, COMMANDS # импортируем настройки и команды
|
||||
|
||||
|
||||
class LoggingMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для логирования апдейтов с определением типа события,
|
||||
пользователя и добавлением префикса проекта к типу лога.
|
||||
|
||||
Автоматически добавляет префикс проекта (например, 'PRIMO-') к типам логов:
|
||||
- PRIMO-UPDATE: общий апдейт без определенного типа
|
||||
- PRIMO-MSG: текстовое сообщение от пользователя
|
||||
- PRIMO-CMD: команда (сообщение, начинающееся с любого префикса)
|
||||
- PRIMO-CBD: callback query от инлайн-кнопок
|
||||
"""
|
||||
|
||||
# Префикс проекта для логов
|
||||
PROJECT_PREFIX: str = "PRIMO"
|
||||
|
||||
# Кэш для всех команд из COMMANDS
|
||||
_all_commands: Optional[Set[str]] = None
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
# Предварительно загружаем все команды
|
||||
self._load_all_commands()
|
||||
|
||||
def _load_all_commands(self) -> None:
|
||||
"""Загружает все команды из COMMANDS в множество для быстрого поиска."""
|
||||
if self._all_commands is None:
|
||||
self._all_commands = set()
|
||||
for command_list in COMMANDS.values():
|
||||
self._all_commands.update(command_list)
|
||||
|
||||
def _is_command(self, text: str) -> bool:
|
||||
"""
|
||||
Проверяет, является ли текст командой с любым префиксом.
|
||||
|
||||
Args:
|
||||
text: Текст для проверки
|
||||
|
||||
Returns:
|
||||
True если это команда, False если нет
|
||||
"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
# Проверяем все префиксы из BotSettings
|
||||
for prefix in BotSettings.PREFIX:
|
||||
if text.startswith(prefix):
|
||||
# Извлекаем команду без префикса
|
||||
command_without_prefix = text[len(prefix):].strip()
|
||||
# Проверяем, есть ли такая команда в нашем списке
|
||||
if command_without_prefix in self._all_commands:
|
||||
return True
|
||||
|
||||
# Также проверяем команды с префиксом / (стандартные)
|
||||
if text.startswith('/'):
|
||||
command_without_slash = text[1:].strip()
|
||||
if command_without_slash in self._all_commands:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _extract_command_name(text: str) -> str:
|
||||
"""
|
||||
Извлекает название команды из текста.
|
||||
|
||||
Args:
|
||||
text: Текст команды с префиксом
|
||||
|
||||
Returns:
|
||||
Название команды без префикса
|
||||
"""
|
||||
for prefix in BotSettings.PREFIX:
|
||||
if text.startswith(prefix):
|
||||
return text[len(prefix):].strip()
|
||||
|
||||
if text.startswith('/'):
|
||||
return text[1:].strip()
|
||||
|
||||
return text
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Обрабатывает входящее событие, определяет его тип, логирует с префиксом проекта
|
||||
и передает следующему обработчику.
|
||||
|
||||
Args:
|
||||
handler: Следующий обработчик в цепочке middleware
|
||||
event: Входящее событие для обработки (Update, Message, CallbackQuery)
|
||||
data: Словарь с контекстными данными FSM
|
||||
|
||||
Returns:
|
||||
Результат выполнения следующего обработчика
|
||||
|
||||
Raises:
|
||||
Exception: Любое исключение, возникшее при обработке хендлером
|
||||
"""
|
||||
# Определяем тип события и информацию для логирования
|
||||
log_type: str
|
||||
log_text: str
|
||||
message_obj: Optional[Message]
|
||||
|
||||
log_type, log_text, message_obj = self._determine_event_type(event)
|
||||
|
||||
# Добавляем префикс проекта к типу лога
|
||||
prefixed_log_type: str = f"{log_type}"
|
||||
|
||||
# Определяем информацию о пользователе
|
||||
user_str: str = self._extract_user_info(event, message_obj)
|
||||
|
||||
# Логируем получение события с префиксом проекта
|
||||
loggers.info(
|
||||
text=log_text,
|
||||
log_type=prefixed_log_type,
|
||||
user=user_str
|
||||
)
|
||||
|
||||
try:
|
||||
# Передаем событие следующему обработчику
|
||||
result: Any = await handler(event, data)
|
||||
|
||||
# Логируем успешное выполнение для команд
|
||||
if log_type == "CMD":
|
||||
loggers.info(
|
||||
text=f"[SUCCESS] команда обработана",
|
||||
log_type=prefixed_log_type,
|
||||
user=user_str
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку при обработке с префиксом проекта
|
||||
loggers.error(
|
||||
text=f"Ошибка обработки: {str(e)}",
|
||||
log_type=prefixed_log_type,
|
||||
user=user_str
|
||||
)
|
||||
raise
|
||||
|
||||
def _determine_event_type(
|
||||
self,
|
||||
event: TelegramObject
|
||||
) -> Tuple[str, str, Optional[Message]]:
|
||||
"""
|
||||
Определяет тип события и извлекает информацию для логирования.
|
||||
|
||||
Args:
|
||||
event: Объект события для анализа
|
||||
|
||||
Returns:
|
||||
Кортеж из (тип_лога, текст_лога, объект_сообщения)
|
||||
"""
|
||||
log_type: str = "UPDATE"
|
||||
log_text: str = f"Получен апдейт: {type(event).__name__}"
|
||||
message_obj: Optional[Message] = None
|
||||
|
||||
# Обработка Update объектов (основной тип в middleware)
|
||||
if isinstance(event, Update):
|
||||
# Пытаемся найти сообщение в различных полях Update
|
||||
message_obj = (
|
||||
event.message or
|
||||
event.edited_message or
|
||||
event.channel_post or
|
||||
event.edited_channel_post
|
||||
)
|
||||
|
||||
if message_obj and message_obj.text:
|
||||
if self._is_command(message_obj.text):
|
||||
log_type: str = "CMD"
|
||||
log_text: str = f"использовал команду '{message_obj.text}'"
|
||||
else:
|
||||
log_type: str = "MSG"
|
||||
log_text: str = f"получено сообщение: {message_obj.text!r}"
|
||||
elif message_obj:
|
||||
# Не текстовое сообщение (фото, видео и т.д.)
|
||||
log_type: str = "MSG"
|
||||
log_text: str = f"получено сообщение: '{type_msg(message_obj)}'"
|
||||
elif event.callback_query:
|
||||
# Обработка callback query
|
||||
callback: CallbackQuery = event.callback_query
|
||||
log_type: str = "CBD"
|
||||
log_text: str = f"получен callback: {callback.data!r}"
|
||||
if callback.message:
|
||||
message_obj: Optional[MaybeInaccessibleMessageUnion] = callback.message
|
||||
|
||||
# Прямая обработка Message (если мидлварь зарегистрирован на messages)
|
||||
elif isinstance(event, Message):
|
||||
message_obj = event
|
||||
if event.text and self._is_command(event.text):
|
||||
log_type: str = "CMD"
|
||||
log_text: str = f"использовал команду '{event.text}'"
|
||||
elif event.text:
|
||||
log_type: str = "MSG"
|
||||
log_text: str = f"получено сообщение: {event.text!r}"
|
||||
else:
|
||||
log_type: str = "MSG"
|
||||
log_text: str = f"получено сообщение типа: {event.content_type}"
|
||||
|
||||
# Прямая обработка CallbackQuery (если мидлварь зарегистрирован на callbacks)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
log_type: str = "CBD"
|
||||
log_text: str = f"получен callback: {event.data!r}"
|
||||
if event.message:
|
||||
message_obj = event.message
|
||||
|
||||
return log_type, log_text, message_obj
|
||||
|
||||
@staticmethod
|
||||
def _extract_user_info(
|
||||
event: TelegramObject,
|
||||
message: Optional[Message] = None
|
||||
) -> str:
|
||||
"""
|
||||
Извлекает информацию о пользователе из события.
|
||||
|
||||
Args:
|
||||
event: Объект события (Update, Message или CallbackQuery)
|
||||
message: Объект Message (если уже определен)
|
||||
|
||||
Returns:
|
||||
Строка с идентификатором пользователя в формате '@username' или 'id<user_id>'
|
||||
"""
|
||||
user_str: str = "@System"
|
||||
|
||||
# Для CallbackQuery извлекаем пользователя из самого callback'а
|
||||
if isinstance(event, CallbackQuery) and hasattr(event, 'from_user') and event.from_user:
|
||||
user = event.from_user
|
||||
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
# Для Message извлекаем пользователя из сообщения
|
||||
elif isinstance(event, Message) and hasattr(event, 'from_user') and event.from_user:
|
||||
user = event.from_user
|
||||
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
# Для Update с callback_query
|
||||
elif (isinstance(event, Update) and
|
||||
event.callback_query and
|
||||
hasattr(event.callback_query, 'from_user') and
|
||||
event.callback_query.from_user):
|
||||
user = event.callback_query.from_user
|
||||
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
# Для Update с сообщением
|
||||
elif (isinstance(event, Update) and
|
||||
(event.message or event.edited_message) and
|
||||
hasattr(event.message or event.edited_message, 'from_user')):
|
||||
msg = event.message or event.edited_message
|
||||
if msg and msg.from_user:
|
||||
user: Optional[User] = msg.from_user
|
||||
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
# Если передан message объект
|
||||
elif message and hasattr(message, 'from_user') and message.from_user:
|
||||
user: Optional[User] = message.from_user
|
||||
user_str: str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
return user_str
|
||||
55
bot/middlewares/msg_mdw.py
Normal file
55
bot/middlewares/msg_mdw.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
from typing import Callable, Dict, Any, Awaitable
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.enums import ChatType
|
||||
from aiogram.types import Message
|
||||
from database import db
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MessageCounterMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для подсчёта сообщений в группах и супергруппах.
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[Message, Dict[str, Any]], Awaitable[Any]],
|
||||
event: Any,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
if not isinstance(event, Message):
|
||||
return await handler(event, data)
|
||||
|
||||
# Проверяем, что сообщение пришло из группового чата и не от бота
|
||||
if (event.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP) and
|
||||
not event.from_user.is_bot):
|
||||
try:
|
||||
await self.process_group_message(event)
|
||||
except Exception as e:
|
||||
logger.error(msg=f"Ошибка при обработке сообщения: {e}", exc_info=True)
|
||||
|
||||
return await handler(event, data)
|
||||
|
||||
@staticmethod
|
||||
async def process_group_message(message: Message) -> None:
|
||||
"""
|
||||
Обработка сообщения из группового чата.
|
||||
"""
|
||||
user_id = message.from_user.id
|
||||
message_text = message.text or message.caption or ""
|
||||
|
||||
# Добавляем пользователя (если его ещё нет)
|
||||
await db.add_user(
|
||||
user_id=user_id,
|
||||
username=message.from_user.username,
|
||||
full_name=message.from_user.full_name,
|
||||
)
|
||||
|
||||
# Сохраняем сообщение
|
||||
await db.add_message(
|
||||
user_id=user_id,
|
||||
message_text=message_text,
|
||||
created_at=message.date,
|
||||
)
|
||||
logger.info(f"Сообщение от пользователя {user_id} сохранено в БД")
|
||||
97
bot/middlewares/spam_mdw.py
Normal file
97
bot/middlewares/spam_mdw.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
from middleware.loggers import loggers # ваш логгер
|
||||
|
||||
|
||||
class RateLimitMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для ограничения частоты запросов от пользователей (анти-спам).
|
||||
|
||||
Зачем нужен:
|
||||
- Защита от DDoS и флуда
|
||||
- Предотвращение злоупотребления ботом
|
||||
- Контроль нагрузки на сервер
|
||||
"""
|
||||
|
||||
def __init__(self, rate_limit: int = 10, time_period: float = 2.0):
|
||||
"""
|
||||
Инициализация rate limit middleware.
|
||||
|
||||
Args:
|
||||
rate_limit: Максимальное количество запросов за период
|
||||
time_period: Период времени в секундах
|
||||
"""
|
||||
self.rate_limit = rate_limit
|
||||
self.time_period = time_period
|
||||
self.user_calls: Dict[int, list[float]] = defaultdict(list)
|
||||
super().__init__()
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
log: bool = False,
|
||||
) -> Any:
|
||||
"""
|
||||
Проверяет rate limit перед обработкой запроса.
|
||||
"""
|
||||
# Пропускаем не-сообщения и не-колбэки
|
||||
if not isinstance(event, (Message, CallbackQuery)):
|
||||
return await handler(event, data)
|
||||
|
||||
user_id: int = event.from_user.id
|
||||
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
|
||||
current_time: float = time.time()
|
||||
|
||||
# Очищаем старые запросы
|
||||
self.user_calls[user_id] = [
|
||||
call_time for call_time in self.user_calls[user_id]
|
||||
if current_time - call_time < self.time_period
|
||||
]
|
||||
|
||||
# Логируем текущее состояние rate limit
|
||||
if log:
|
||||
loggers.debug(
|
||||
text=f"Rate limit: {len(self.user_calls[user_id])}/{self.rate_limit} за {self.time_period}сек",
|
||||
log_type="RATE_LIMIT_STATUS",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Проверяем текущий лимит
|
||||
if len(self.user_calls[user_id]) >= self.rate_limit:
|
||||
# Логируем попытку спама
|
||||
if log:
|
||||
loggers.warning(
|
||||
text=f"Превышен rate limit ({self.rate_limit}/{self.time_period}сек)",
|
||||
log_type="RATE_LIMIT_EXCEEDED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Отправляем сообщение о превышении лимита
|
||||
if isinstance(event, Message):
|
||||
await event.answer(
|
||||
text="⏳ Слишком много запросов! Пожалуйста, подождите немного.",
|
||||
)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.answer(
|
||||
text="⏳ Подождите немного перед следующим действием.",
|
||||
show_alert=True
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# Добавляем текущий запрос и продолжаем обработку
|
||||
self.user_calls[user_id].append(current_time)
|
||||
|
||||
loggers.debug(
|
||||
text=f"Запрос добавлен в rate limit",
|
||||
log_type="RATE_LIMIT_ADDED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
return await handler(event, data)
|
||||
115
bot/middlewares/subscription_mdw.py
Normal file
115
bot/middlewares/subscription_mdw.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from middleware.loggers import loggers # ваш логгер
|
||||
|
||||
|
||||
class SubscriptionMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для проверки подписки пользователя на необходимые каналы.
|
||||
Блокирует обработку команд, если пользователь не подписан.
|
||||
|
||||
Зачем нужен:
|
||||
- Автоматическая проверка подписки для всех входящих сообщений
|
||||
- Единая точка управления подписками
|
||||
- Предотвращение доступа к функционалу без подписки
|
||||
"""
|
||||
|
||||
def __init__(self, bot: Bot, channel_ids: list[int | str]):
|
||||
"""
|
||||
Инициализация middleware проверки подписки.
|
||||
|
||||
Args:
|
||||
bot: Экземпляр бота
|
||||
channel_ids: Список ID каналов/чатов для проверки подписки
|
||||
"""
|
||||
self.bot = bot
|
||||
self.channel_ids = channel_ids
|
||||
super().__init__()
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Проверяет подписку пользователя перед обработкой команды.
|
||||
"""
|
||||
# Пропускаем не-сообщения и не-колбэки
|
||||
if not isinstance(event, (Message, CallbackQuery)):
|
||||
return await handler(event, data)
|
||||
|
||||
user_id: int = event.from_user.id
|
||||
user_str: str = f"@{event.from_user.username}" if event.from_user.username else f"id{user_id}"
|
||||
|
||||
# Логируем начало проверки подписки
|
||||
loggers.info(
|
||||
text=f"Проверка подписки для пользователя",
|
||||
log_type="SUBSCRIPTION_CHECK",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Проверяем подписку на все required каналы
|
||||
not_subscribed_channels: list[str] = []
|
||||
|
||||
for channel_id in self.channel_ids:
|
||||
try:
|
||||
member = await self.bot.get_chat_member(
|
||||
chat_id=channel_id,
|
||||
user_id=user_id
|
||||
)
|
||||
# Проверяем, что пользователь является участником
|
||||
if member.status not in ['member', 'administrator', 'creator']:
|
||||
not_subscribed_channels.append(str(channel_id))
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
loggers.error(
|
||||
text=f"Ошибка проверки подписки на канал {channel_id}: {e}",
|
||||
log_type="SUBSCRIPTION_ERROR",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Если пользователь не подписан на некоторые каналы
|
||||
if not_subscribed_channels:
|
||||
loggers.warning(
|
||||
text=f"Пользователь не подписан на каналы: {', '.join(not_subscribed_channels)}",
|
||||
log_type="SUBSCRIPTION_FAILED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
warning_text = (
|
||||
"📢 Для использования бота необходимо подписаться на наши каналы!\n\n"
|
||||
"После подписки нажмите /start для продолжения."
|
||||
)
|
||||
|
||||
# Создаем кнопку "Проверить подписку"
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[[
|
||||
InlineKeyboardButton(
|
||||
text="✅ Я подписался",
|
||||
callback_data="check_subscription"
|
||||
)
|
||||
]]
|
||||
)
|
||||
|
||||
if isinstance(event, Message):
|
||||
await event.answer(warning_text, reply_markup=keyboard)
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.message.answer(warning_text, reply_markup=keyboard)
|
||||
await event.answer()
|
||||
|
||||
return None
|
||||
|
||||
# Логируем успешную проверку подписки
|
||||
loggers.info(
|
||||
text="Пользователь подписан на все required каналы",
|
||||
log_type="SUBSCRIPTION_SUCCESS",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Если подписка есть, продолжаем обработку
|
||||
return await handler(event, data)
|
||||
82
bot/middlewares/time_mdw.py
Normal file
82
bot/middlewares/time_mdw.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
|
||||
from time import time
|
||||
|
||||
from middleware.loggers import loggers # ваш логгер
|
||||
|
||||
|
||||
class TimingMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для измерения времени выполнения хендлеров.
|
||||
|
||||
Зачем нужен:
|
||||
- Мониторинг производительности хендлеров
|
||||
- Выявление медленных запросов
|
||||
- Оптимизация кода бота
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
perm: str = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Измеряет время выполнения хендлера.
|
||||
"""
|
||||
start_time: float = time()
|
||||
|
||||
try:
|
||||
result = await handler(event, data)
|
||||
return result
|
||||
|
||||
finally:
|
||||
execution_time: float = time() - start_time
|
||||
|
||||
# Получаем информацию о пользователе безопасным способом
|
||||
user_str: str = "@System"
|
||||
|
||||
# Для Message и CallbackQuery
|
||||
if isinstance(event, (Message, CallbackQuery)) and hasattr(event, 'from_user') and event.from_user:
|
||||
user = event.from_user
|
||||
user_str = f"@{user.username}" if user.username else f"id{user.id}"
|
||||
|
||||
# Для Update (который содержит message или callback_query)
|
||||
elif isinstance(event, Update):
|
||||
# Пытаемся найти пользователя в различных полях Update
|
||||
user_object = None
|
||||
if event.message and event.message.from_user:
|
||||
user_object = event.message.from_user
|
||||
elif event.edited_message and event.edited_message.from_user:
|
||||
user_object = event.edited_message.from_user
|
||||
elif event.callback_query and event.callback_query.from_user:
|
||||
user_object = event.callback_query.from_user
|
||||
elif event.channel_post and event.channel_post.from_user:
|
||||
user_object = event.channel_post.from_user
|
||||
elif event.edited_channel_post and event.edited_channel_post.from_user:
|
||||
user_object = event.edited_channel_post.from_user
|
||||
|
||||
if user_object:
|
||||
user_str = f"@{user_object.username}" if user_object.username else f"id{user_object.id}"
|
||||
|
||||
# Логируем время выполнения
|
||||
if execution_time > 1.0 and perm: # Медленные запросы
|
||||
loggers.warning(
|
||||
text=f"Медленный хендлер: {execution_time:.2f}сек",
|
||||
log_type="SLOW_HANDLER",
|
||||
user=user_str
|
||||
)
|
||||
elif execution_time > 0.5 and perm == "medium": # Средние запросы
|
||||
loggers.info(
|
||||
text=f"Среднее время выполнения: {execution_time:.3f}сек",
|
||||
log_type="HANDLER_TIMING",
|
||||
user=user_str
|
||||
)
|
||||
elif perm == "fast": # Быстрые запросы
|
||||
loggers.debug(
|
||||
text=f"Быстрое выполнение: {execution_time:.3f}сек",
|
||||
log_type="HANDLER_TIMING_FAST",
|
||||
user=user_str
|
||||
)
|
||||
0
bot/states/__init__.py
Normal file
0
bot/states/__init__.py
Normal file
5
bot/states/anketa_states.py
Normal file
5
bot/states/anketa_states.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# bot/states/form.py
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class StartForm(StatesGroup):
|
||||
waiting_for_application: State = State()
|
||||
8
bot/states/new_states.py
Normal file
8
bot/states/new_states.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# bot/states/new_states.py
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class NewStates(StatesGroup):
|
||||
role: State = State()
|
||||
sorol: State = State()
|
||||
code_phrase: State = State()
|
||||
rules: State = State()
|
||||
1
bot/templates/__init__.py
Normal file
1
bot/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .message_callback import *
|
||||
77
bot/templates/message_callback.py
Normal file
77
bot/templates/message_callback.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from typing import Union
|
||||
|
||||
from aiogram.types import FSInputFile, CallbackQuery, Message, ReplyKeyboardMarkup, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
||||
|
||||
# Настройка экспорта
|
||||
__all__ = ('msg', 'msg_photo')
|
||||
|
||||
|
||||
async def msg(message: Message | CallbackQuery,
|
||||
text: str = "Сообщение отправлено!",
|
||||
markup: Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, None] = None) -> None:
|
||||
"""
|
||||
Шаблон для ответа на сообщение текстом.
|
||||
:param message: Объект сообщения или callback-запроса.
|
||||
:param text: Текст отправного сообщения от бота.
|
||||
:param markup: Кнопки сообщения (инлайн или реплай).
|
||||
"""
|
||||
|
||||
# Преобразуем клавиатуру
|
||||
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
|
||||
if markup:
|
||||
if isinstance(markup, InlineKeyboardBuilder):
|
||||
reply_markup: InlineKeyboardMarkup = markup.as_markup()
|
||||
elif isinstance(markup, ReplyKeyboardBuilder):
|
||||
reply_markup: ReplyKeyboardMarkup = markup.as_markup(resize_keyboard=True)
|
||||
|
||||
# Обработчик ответа на сообщение
|
||||
if isinstance(message, Message):
|
||||
await message.reply(
|
||||
text=text,
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
# Обработчик ответа на callback
|
||||
else:
|
||||
await message.message.reply(
|
||||
text=text,
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
|
||||
|
||||
async def msg_photo(
|
||||
message: Message | CallbackQuery,
|
||||
text: str = "Сообщение отправлено!",
|
||||
file: str = "assets/default.jpg",
|
||||
markup: Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, None] = None) -> None:
|
||||
"""
|
||||
Шаблон для ответа на сообщение фотографией.
|
||||
:param message: Объект сообщения или callback-запроса.
|
||||
:param file: Путь к фотографии для ответа.
|
||||
:param text: Подпись к фото.
|
||||
:param markup: Кнопки сообщения (инлайн или реплай).
|
||||
"""
|
||||
|
||||
# Преобразуем клавиатуру
|
||||
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup, None] = None
|
||||
if markup:
|
||||
if isinstance(markup, InlineKeyboardBuilder):
|
||||
reply_markup = markup.as_markup()
|
||||
elif isinstance(markup, ReplyKeyboardBuilder):
|
||||
reply_markup = markup.as_markup(resize_keyboard=True)
|
||||
|
||||
# Обработчик ответа на сообщение
|
||||
if isinstance(message, Message):
|
||||
await message.reply_photo(
|
||||
photo=FSInputFile(file),
|
||||
caption=text,
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
|
||||
# Обработчик ответа на callback
|
||||
else:
|
||||
await message.message.reply_photo(
|
||||
photo=FSInputFile(file),
|
||||
caption=text,
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
5
bot/utils/__init__.py
Normal file
5
bot/utils/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from .interesting_facts import *
|
||||
from .usernames import *
|
||||
from .pagination import *
|
||||
from .type_message import *
|
||||
from .argument import *
|
||||
59
bot/utils/argument.py
Normal file
59
bot/utils/argument.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from configs import BotSettings
|
||||
|
||||
__all__ = ("is_command", "find_argument")
|
||||
|
||||
|
||||
def is_command(message: Optional[str]) -> bool:
|
||||
"""
|
||||
Проверяет, является ли сообщение командой.
|
||||
|
||||
Сообщение считается командой, если:
|
||||
1. Оно не пустое;
|
||||
2. Начинается с префикса команды, указанного в настройках.
|
||||
|
||||
Args:
|
||||
message (Optional[str]): Входное сообщение.
|
||||
|
||||
Returns:
|
||||
bool: True, если сообщение является командой, иначе False.
|
||||
|
||||
Пример:
|
||||
>>> is_command("/start")
|
||||
True
|
||||
>>> is_command("hello")
|
||||
False
|
||||
"""
|
||||
if not message:
|
||||
return False
|
||||
return message.strip().startswith(BotSettings.PREFIX)
|
||||
|
||||
|
||||
def find_argument(message: Optional[str]) -> Optional[str]:
|
||||
"""
|
||||
Извлекает аргумент команды из сообщения.
|
||||
|
||||
Аргументом считается текст после первой команды и пробела.
|
||||
Если аргумента нет — возвращает None.
|
||||
|
||||
Args:
|
||||
message (Optional[str]): Входное сообщение.
|
||||
|
||||
Returns:
|
||||
Optional[str]: Аргумент команды или None, если его нет.
|
||||
|
||||
Пример:
|
||||
>>> find_argument("/start referrer")
|
||||
'referrer'
|
||||
>>> find_argument("/start")
|
||||
None
|
||||
>>> find_argument("hello")
|
||||
None
|
||||
"""
|
||||
if not is_command(message):
|
||||
return None
|
||||
|
||||
parts = message.strip().split(maxsplit=1)
|
||||
return parts[1] if len(parts) > 1 else None
|
||||
54
bot/utils/interesting_facts.py
Normal file
54
bot/utils/interesting_facts.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from random import choice
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from configs.config import Lists
|
||||
|
||||
__all__ = ("interesting_fact", "get_best_response",)
|
||||
|
||||
def interesting_fact(mode: str = "факт", lists: Optional[list[str]] = None) -> str:
|
||||
"""
|
||||
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
|
||||
|
||||
:param mode: строка, определяющая тип контента ("факт", "анекдот", "цитата").
|
||||
:param lists: необязательный список строк, из которого можно выбирать вручную.
|
||||
:return: случайный элемент из соответствующего списка.
|
||||
"""
|
||||
if lists is not None:
|
||||
return choice(lists)
|
||||
|
||||
mode = mode.lower()
|
||||
|
||||
if mode == "анекдот":
|
||||
source: list[str] = Lists.jokes
|
||||
elif mode == "цитата":
|
||||
source: list[str] = Lists.quotes
|
||||
else:
|
||||
source: list[str] = Lists.facts
|
||||
|
||||
return choice(source)
|
||||
|
||||
|
||||
def get_best_response(
|
||||
user_text: str,
|
||||
responses: Dict[str, Dict[str, List[str]]],
|
||||
random_phrases: List[str],
|
||||
) -> str:
|
||||
"""
|
||||
Подбирает наиболее подходящий ответ на сообщение пользователя.
|
||||
Сначала ищет ключевые слова и их синонимы, если совпадений нет — выдаёт случайную фразу.
|
||||
|
||||
:param user_text: текст сообщения пользователя
|
||||
:param responses: словарь с ключевыми словами и ответами
|
||||
:param random_phrases: список случайных фраз, если совпадений нет
|
||||
:return: строка с ответом
|
||||
"""
|
||||
normalized_text: str = user_text.lower()
|
||||
|
||||
# Перебор ключевых слов в словаре
|
||||
for _, data in responses.items():
|
||||
for keyword in data["keywords"]:
|
||||
if keyword in normalized_text:
|
||||
return choice(data["answers"])
|
||||
|
||||
# Если совпадений нет — выдаём случайную фразу
|
||||
return choice(random_phrases)
|
||||
28
bot/utils/pagination.py
Normal file
28
bot/utils/pagination.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from aiogram.types import InlineKeyboardButton
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ('pagination_btn',)
|
||||
|
||||
def pagination_btn(action: str,
|
||||
page: int = 0,
|
||||
total_posts: int = 0,
|
||||
bt_page: int = 5) -> list[InlineKeyboardButton]:
|
||||
"""
|
||||
Создает кнопки для пагинации.
|
||||
|
||||
:param action: Действие в котором нужна пангинация.
|
||||
:param page: Номер начальной страницы, по умолчанию 0.
|
||||
:param total_posts: Количество постов.
|
||||
:param bt_page: Количество кнопок на одной странице.
|
||||
:return: Готовый лист списка инлайн-кнопок.
|
||||
"""
|
||||
navigation_buttons: list[InlineKeyboardButton] = []
|
||||
if page > 0:
|
||||
navigation_buttons.append(InlineKeyboardButton(
|
||||
text="←", callback_data=f"{action}_page_{page - 1}"
|
||||
))
|
||||
if (page + 1) * bt_page < total_posts:
|
||||
navigation_buttons.append(InlineKeyboardButton(
|
||||
text="→", callback_data=f"{action}_page_{page + 1}"
|
||||
))
|
||||
return navigation_buttons
|
||||
18
bot/utils/random_lists.py
Normal file
18
bot/utils/random_lists.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from random import choice
|
||||
|
||||
def get_best_response(user_text: str) -> str:
|
||||
"""
|
||||
Подбирает наиболее подходящий ответ на сообщение пользователя.
|
||||
Сначала ищет ключевые слова и их синонимы, если совпадений нет — выдаёт случайную фразу.
|
||||
|
||||
:param user_text: текст сообщения пользователя
|
||||
:return: строка с ответом
|
||||
"""
|
||||
normalized_text: str = user_text.lower()
|
||||
|
||||
for _, data in RESPONSES.items():
|
||||
for keyword in data["keywords"]:
|
||||
if keyword in normalized_text:
|
||||
return choice(data["answers"])
|
||||
|
||||
return choice(RANDOM_PHRASES)
|
||||
85
bot/utils/type_message.py
Normal file
85
bot/utils/type_message.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from typing import Final
|
||||
|
||||
from aiogram.types import Message
|
||||
|
||||
# Настройка экспорта
|
||||
__all__ = ("CHAT_TYPES", "CONTENT_TYPE_RU", "type_chat", "type_msg")
|
||||
|
||||
|
||||
# Словарь сопоставлений "chat_type -> русское название"
|
||||
CHAT_TYPES: Final[dict[str, str]] = {
|
||||
"private": "Личный",
|
||||
"group": "Группа",
|
||||
"supergroup": "Группа",
|
||||
"channel": "Канал",
|
||||
}
|
||||
|
||||
# Словарь сопоставлений "content_type -> русское название"
|
||||
CONTENT_TYPE_RU: Final[dict[str, str]] = {
|
||||
"text": "Текст",
|
||||
"animation": "Гиф",
|
||||
"audio": "Аудио",
|
||||
"document": "Файл",
|
||||
"photo": "Фото",
|
||||
"sticker": "Стикер",
|
||||
"video": "Видео",
|
||||
"video_note": "Видеосообщение",
|
||||
"voice": "Голосовое сообщение",
|
||||
"contact": "Контакт",
|
||||
"dice": "Кубик",
|
||||
"game": "Игра",
|
||||
"poll": "Опрос",
|
||||
"venue": "Место",
|
||||
"location": "Локация",
|
||||
"new_chat_members": "Новые участники чата",
|
||||
"left_chat_member": "Участник вышел",
|
||||
"new_chat_title": "Новое название чата",
|
||||
"new_chat_photo": "Новая картинка чата",
|
||||
"delete_chat_photo": "Удалена картинка чата",
|
||||
"group_chat_created": "Создана группа",
|
||||
"supergroup_chat_created": "Создана супергруппа",
|
||||
"channel_chat_created": "Создан канал",
|
||||
"message_auto_delete_timer_changed": "Изменён автоудалитель",
|
||||
"migrate_to_chat_id": "Группа → супергруппа",
|
||||
"migrate_from_chat_id": "Супергруппа → группа",
|
||||
"pinned_message": "Закреплённое сообщение",
|
||||
"invoice": "Счёт",
|
||||
"successful_payment": "Успешный платёж",
|
||||
"connected_website": "Подключённый сайт",
|
||||
"passport_data": "Данные Telegram Passport",
|
||||
"proximity_alert_triggered": "Алерт о приближении",
|
||||
"video_chat_scheduled": "Запланированный видеочат",
|
||||
"video_chat_started": "Видеочат начался",
|
||||
"video_chat_ended": "Видеочат завершён",
|
||||
"video_chat_participants_invited": "Приглашены участники видеочата",
|
||||
"web_app_data": "Данные из веб-приложения",
|
||||
"forum_topic_created": "Создана тема форума",
|
||||
"forum_topic_edited": "Изменена тема форума",
|
||||
"forum_topic_closed": "Тема форума закрыта",
|
||||
"forum_topic_reopened": "Тема форума открыта",
|
||||
"general_forum_topic_hidden": "Общая тема скрыта",
|
||||
"general_forum_topic_unhidden": "Общая тема снова отображается",
|
||||
"giveaway_created": "Создан розыгрыш",
|
||||
"giveaway": "Розыгрыш",
|
||||
"giveaway_completed": "Розыгрыш завершён",
|
||||
"message_reaction": "Реакция на сообщение",
|
||||
}
|
||||
|
||||
|
||||
def type_msg(message: Message) -> str:
|
||||
"""
|
||||
Определяет и возвращает тип сообщения на русском языке.
|
||||
|
||||
:param message: объект Message от aiogram
|
||||
:return: строка с типом сообщения
|
||||
"""
|
||||
return CONTENT_TYPE_RU.get(message.content_type, f"Неизвестный тип ({message.content_type})")
|
||||
|
||||
def type_chat(message: Message) -> str:
|
||||
"""
|
||||
Преобразует информацию о чате в его тип на русском языке.
|
||||
|
||||
:param message: Объект сообщения из aiogram, содержащий информацию о чате.
|
||||
:return: Тип чата строкой.
|
||||
"""
|
||||
return CHAT_TYPES.get(message.chat.type, f"Неизвестный тип чата {message.chat.type}")
|
||||
21
bot/utils/usernames.py
Normal file
21
bot/utils/usernames.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram.types import Message
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ('username', )
|
||||
|
||||
# Функция получения юзера или ID пользователя
|
||||
def username(message: Message) -> str:
|
||||
"""
|
||||
Возвращает юзернейм пользователя из сообщения, или ID, если юзернейм не указан.
|
||||
|
||||
:param message: Объект сообщения из aiogram.
|
||||
:return: Строка с юзернеймом пользователя или его ID.
|
||||
:raises ValueError: Если в сообщении отсутствует информация о пользователе.
|
||||
"""
|
||||
try:
|
||||
if message.from_user:
|
||||
return f"@{message.from_user.username}" if message.from_user.username else f"@{message.from_user.id}"
|
||||
raise ValueError("Информация о пользователе отсутствует в сообщении.")
|
||||
|
||||
except ValueError as e:
|
||||
raise e # Перебрасываем ошибку выше для дальнейшей обработки
|
||||
Reference in New Issue
Block a user