First commit
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 *
|
||||
260
bot/core/bots.py
Normal file
260
bot/core/bots.py
Normal file
@@ -0,0 +1,260 @@
|
||||
from asyncio import sleep
|
||||
from datetime import datetime
|
||||
|
||||
from aiogram import Bot, Dispatcher
|
||||
from aiogram.client.default import DefaultBotProperties
|
||||
from aiogram.exceptions import TelegramRetryAfter
|
||||
from aiogram.fsm.storage.memory import MemoryStorage
|
||||
from aiogram.types import User, ChatAdministratorRights, BotDescription, BotShortDescription
|
||||
from aiogram.utils.i18n import I18n, SimpleI18nMiddleware
|
||||
|
||||
from configs.config import BotSettings, BotEdit, Webhook, Permission
|
||||
from middleware.loggers import log, logger
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("dp", "bot", "BotInfo", "i18n")
|
||||
|
||||
# FSM-хранилище и диспетчер
|
||||
storage: MemoryStorage = MemoryStorage()
|
||||
dp: Dispatcher = Dispatcher(storage=storage)
|
||||
dp["is_active"]: bool = True
|
||||
|
||||
# Локализация
|
||||
i18n: I18n = I18n(path="locales", default_locale="ru", domain="bot")
|
||||
i18n_middleware: SimpleI18nMiddleware = SimpleI18nMiddleware(i18n=i18n)
|
||||
i18n_middleware.setup(dp)
|
||||
|
||||
# Экземпляр бота
|
||||
bot: Bot = Bot(
|
||||
token=BotSettings.BOT_TOKEN,
|
||||
default=DefaultBotProperties(
|
||||
parse_mode=BotSettings.PARSE_MODE,
|
||||
disable_notification=BotSettings.DISABLE_NOTIFICATION,
|
||||
protect_content=BotSettings.PROTECT_CONTENT,
|
||||
allow_sending_without_reply=BotSettings.ALLOW_SENDING_WITHOUT_REPLY,
|
||||
link_preview_is_disabled=BotSettings.LINK_PREVIEW_IS_DISABLED,
|
||||
link_preview_prefer_small_media=BotSettings.LINK_PREVIEW_PREFER_SMALL_MEDIA,
|
||||
link_preview_prefer_large_media=BotSettings.LINK_PREVIEW_PREFER_LARGE_MEDIA,
|
||||
link_preview_show_above_text=BotSettings.LINK_PREVIEW_SHOW_ABOVE_TEXT,
|
||||
show_caption_above_media=BotSettings.SHOW_CAPTION_ABOVE_MEDIA,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BotInfo:
|
||||
"""
|
||||
Класс для хранения и управления информацией о боте.
|
||||
Все поля строго аннотированы, description заменено на widget.
|
||||
"""
|
||||
|
||||
id: int | None = None
|
||||
url: str | None = None
|
||||
first_name: str | None = None
|
||||
last_name: str | None = None
|
||||
username: str | None = None
|
||||
widget: str | None = None # вместо description
|
||||
description: str | None = None # вместо short_description
|
||||
language_code: str = BotSettings.BOT_LANGUAGE
|
||||
prefix: str = BotSettings.PREFIX
|
||||
bot_owner: str = BotSettings.OWNER
|
||||
added_to_attachment_menu: bool = False
|
||||
supports_inline_queries: bool = False
|
||||
can_connect_to_business: bool = False
|
||||
has_main_web_app: bool = False
|
||||
can_join_groups: bool = False
|
||||
can_read_all_group_messages: bool = False
|
||||
rights: ChatAdministratorRights | None = None
|
||||
|
||||
@classmethod
|
||||
@log(level="INFO", log_type="BOT", text="Настройка вебхука бота")
|
||||
async def webhook(
|
||||
cls, bots: Bot = bot, webhook_url: str = Webhook.WEBHOOK_URL, use_webhook: bool = Webhook.WEBHOOK
|
||||
) -> None:
|
||||
"""
|
||||
Установка или удаление вебхука для бота.
|
||||
"""
|
||||
try:
|
||||
await bots.delete_webhook(drop_pending_updates=True)
|
||||
if use_webhook:
|
||||
if webhook_url is None:
|
||||
raise ValueError("Для установки вебхука необходимо указать webhook_url")
|
||||
try:
|
||||
await bots.set_webhook(webhook_url)
|
||||
except TelegramRetryAfter as e:
|
||||
logger.warning(f"Flood control при установке вебхука. Повтор через {e.retry_after} сек.")
|
||||
await sleep(e.retry_after)
|
||||
await bots.set_webhook(webhook_url)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при настройке вебхука: {e}")
|
||||
|
||||
@classmethod
|
||||
@log(level="INFO", log_type="BOT", text="Получение информации о боте")
|
||||
async def info(cls, bots: Bot = bot) -> dict[str, object] | None:
|
||||
"""
|
||||
Получает и сохраняет основные данные о боте.
|
||||
"""
|
||||
try:
|
||||
bot_info: User = await bots.get_me()
|
||||
bot_description: BotDescription = await bots.get_my_description()
|
||||
bot_short_description: BotShortDescription = await bots.get_my_short_description()
|
||||
bot_rights: ChatAdministratorRights = await bot.get_my_default_administrator_rights()
|
||||
|
||||
cls.id = bot_info.id
|
||||
cls.url = f"tg://user?id={cls.id}"
|
||||
cls.first_name = bot_info.first_name
|
||||
cls.last_name = bot_info.last_name
|
||||
cls.username = bot_info.username
|
||||
cls.language_code = bot_info.language_code
|
||||
|
||||
# Описание (widget) и короткое описание (description)
|
||||
cls.widget = bot_description.description or ""
|
||||
cls.description = bot_short_description.short_description or ""
|
||||
|
||||
cls.added_to_attachment_menu = getattr(bot_info, "added_to_attachment_menu", False)
|
||||
cls.supports_inline_queries = getattr(bot_info, "supports_inline_queries", False)
|
||||
cls.can_connect_to_business = getattr(bot_info, "can_connect_to_business", False)
|
||||
cls.has_main_web_app = getattr(bot_info, "has_main_web_app", False)
|
||||
cls.can_join_groups = getattr(bot_info, "can_join_groups", False)
|
||||
cls.can_read_all_group_messages = getattr(bot_info, "can_read_all_group_messages", False)
|
||||
cls.rights = bot_rights or None
|
||||
|
||||
return {
|
||||
"id": cls.id,
|
||||
"url": cls.url,
|
||||
"first_name": cls.first_name,
|
||||
"last_name": cls.last_name,
|
||||
"username": cls.username,
|
||||
"language_code": cls.language_code,
|
||||
"widget": cls.widget,
|
||||
"description": cls.description,
|
||||
"added_to_attachment_menu": cls.added_to_attachment_menu,
|
||||
"supports_inline_queries": cls.supports_inline_queries,
|
||||
"can_connect_to_business": cls.can_connect_to_business,
|
||||
"has_main_web_app": cls.has_main_web_app,
|
||||
"can_join_groups": cls.can_join_groups,
|
||||
"can_read_all_group_messages": cls.can_read_all_group_messages,
|
||||
"prefix": cls.prefix,
|
||||
"bot_owner": cls.bot_owner,
|
||||
"rights": cls.rights,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении информации о боте: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Установка прав администратора")
|
||||
async def set_administrator_rights(rights: ChatAdministratorRights = BotEdit.RIGHTS, bots: Bot = bot) -> None:
|
||||
"""
|
||||
Устанавливает дефолтные права администратора для бота.
|
||||
"""
|
||||
try:
|
||||
current_rights: ChatAdministratorRights = await bots.get_my_default_administrator_rights()
|
||||
if current_rights != rights:
|
||||
await bots.set_my_default_administrator_rights(rights=rights)
|
||||
await bots.set_my_default_administrator_rights(rights=rights, for_channels=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при установке прав администратора: {e}")
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Обновление имени бота")
|
||||
async def set_name(new_name: str = BotEdit.NAME, bots: Bot = bot) -> None:
|
||||
"""
|
||||
Обновляет имя бота (от 1 до 32 символов).
|
||||
"""
|
||||
try:
|
||||
current_name: str = (await bots.get_me()).first_name
|
||||
if not (1 <= len(new_name) <= 32):
|
||||
raise ValueError("Имя бота должно быть от 1 до 32 символов.")
|
||||
if current_name != new_name:
|
||||
await bots.set_my_name(name=new_name)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении имени бота: {e}")
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Обновление виджета бота")
|
||||
async def set_widget(new_widget: str = BotEdit.DESCRIPTION, bots: Bot = bot) -> None:
|
||||
"""
|
||||
Обновляет описание бота (widget).
|
||||
"""
|
||||
try:
|
||||
current_widget: BotDescription = await bots.get_my_description()
|
||||
if not (0 < len(new_widget) <= 255):
|
||||
raise ValueError("Виджет должен быть от 1 до 255 символов.")
|
||||
if current_widget.description != new_widget:
|
||||
await bots.set_my_description(description=new_widget)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении виджета бота: {e}")
|
||||
|
||||
@staticmethod
|
||||
@log(level="INFO", log_type="BOT", text="Обновление короткого виджета бота")
|
||||
async def set_short_widget(new_short: str = BotEdit.SHORT_DESCRIPTION, bots: Bot = bot) -> None:
|
||||
"""
|
||||
Обновляет короткое описание (short_widget).
|
||||
"""
|
||||
try:
|
||||
current_short: BotShortDescription = await bots.get_my_short_description()
|
||||
if not (0 < len(new_short) <= 120):
|
||||
raise ValueError("Короткий виджет должен быть от 1 до 120 символов.")
|
||||
if current_short.short_description != new_short:
|
||||
await bots.set_my_short_description(short_description=new_short)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обновлении короткого виджета бота: {e}")
|
||||
|
||||
@staticmethod
|
||||
def start_info_out(out: bool = True) -> str | None:
|
||||
"""
|
||||
Формирует и выводит стартовую информацию о боте.
|
||||
"""
|
||||
try:
|
||||
bot_time: str = f"Бот @{BotInfo.username} запущен в {datetime.now().strftime('%S:%M:%H %d-%m-%Y')}\n"
|
||||
bot_name: str = f"Основное имя: {BotInfo.first_name}\n"
|
||||
bot_postname: str = f" Доп. имя: {BotInfo.last_name}\n"
|
||||
bot_description: str = f" Описание бота: {BotInfo.description}\n"
|
||||
bot_widget: str = f" Виджет бота: {BotInfo.widget}\n"
|
||||
bot_username: str = f" Юзернейм: @{BotInfo.username}\n"
|
||||
bot_id: str = f" ID: {BotInfo.id}\n"
|
||||
bot_can_join_groups: str = f" Может ли вступать в группы: {BotInfo.can_join_groups}\n"
|
||||
bot_can_read_all_group_messages: str = f" Чтение всех сообщений: {BotInfo.can_read_all_group_messages}\n"
|
||||
bot_added_to_attachment_menu: str = f" Добавлен в меню вложений: {BotInfo.added_to_attachment_menu}\n"
|
||||
bot_supports_inline_queries: str = f" Поддерживает инлайн-запросы: {BotInfo.supports_inline_queries}\n"
|
||||
bot_can_connect_to_business: str = f" Подключение к бизнес-аккаунтам: {BotInfo.can_connect_to_business}\n"
|
||||
bot_has_main_web_app: str = f" Основное веб-приложение: {BotInfo.has_main_web_app}\n"
|
||||
bot_prefixs: str = f" Префиксы команд бота: {BotInfo.prefix}\n"
|
||||
|
||||
bot_all_info: str = (
|
||||
f"{bot_name} {bot_postname} {bot_description} {bot_widget} {bot_username} "
|
||||
f"{bot_id} {bot_can_join_groups} {bot_can_read_all_group_messages} "
|
||||
f"{bot_added_to_attachment_menu} {bot_supports_inline_queries} "
|
||||
f"{bot_can_connect_to_business} {bot_has_main_web_app} {bot_prefixs}"
|
||||
)
|
||||
|
||||
if out:
|
||||
print(f"\033[34m{bot_all_info}\033[0m")
|
||||
|
||||
with open("Logs/info.log", "w", encoding="utf-8") as log_file:
|
||||
log_file.write(f"{bot_time}{bot_all_info}")
|
||||
|
||||
with open("Logs/bot_start.log", "a", encoding="utf-8") as log_start_file:
|
||||
log_start_file.write(f"{bot_time}\n")
|
||||
|
||||
return bot_all_info
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при выводе стартовой информации: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
@log(level="INFO", log_type="START", text="Процесс запуска бота!")
|
||||
async def setup(cls, perm: bool = Permission.BOT_EDIT, bots: Bot = bot) -> None:
|
||||
"""
|
||||
Настройка и инициализация всех параметров бота при старте.
|
||||
"""
|
||||
try:
|
||||
await cls.webhook(bots=bots)
|
||||
await cls.info(bots=bots)
|
||||
if perm:
|
||||
await cls.set_administrator_rights(bots=bots)
|
||||
await cls.set_widget(bots=bots)
|
||||
await cls.set_short_widget(bots=bots)
|
||||
await cls.set_name(bots=bots)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при запуске настройки бота: {e}")
|
||||
53
bot/core/webhook.py
Normal file
53
bot/core/webhook.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import web
|
||||
from aiogram.types import Update
|
||||
|
||||
from middleware.loggers import logger
|
||||
from bot.core.bots import dp, bot
|
||||
|
||||
# Настройки экспорта в модули
|
||||
__all__ = ("WebhookApp",)
|
||||
|
||||
|
||||
class WebhookApp:
|
||||
"""Приложение aiohttp для обработки webhook-запросов."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8080) -> None:
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.app: web.Application = web.Application()
|
||||
self.app.router.add_post("/webhook", self.handle_update)
|
||||
self.runner: web.AppRunner | None = None
|
||||
self.site: web.TCPSite | None = None
|
||||
|
||||
@staticmethod
|
||||
async def handle_update(request: web.Request) -> web.Response:
|
||||
"""Обработчик входящих запросов от Telegram."""
|
||||
try:
|
||||
update_json: dict[str, Any] = await request.json()
|
||||
update: Update = Update.model_validate(update_json)
|
||||
await dp.feed_update(bot=bot, update=update)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки webhook-запроса: {e}")
|
||||
return web.Response(status=500)
|
||||
|
||||
return web.Response(status=200)
|
||||
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Асинхронный запуск aiohttp-приложения."""
|
||||
self.runner = web.AppRunner(self.app)
|
||||
await self.runner.setup()
|
||||
|
||||
self.site = web.TCPSite(self.runner, self.host, self.port)
|
||||
await self.site.start()
|
||||
|
||||
logger.info(f"🌍 Webhook сервер запущен на http://{self.host}:{self.port}")
|
||||
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Остановка aiohttp-приложения."""
|
||||
if self.runner:
|
||||
await self.runner.cleanup()
|
||||
1
bot/data/__init__.py
Normal file
1
bot/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .topic_map import *
|
||||
4
bot/data/topic_map.py
Normal file
4
bot/data/topic_map.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# bot/data/topic_map.py
|
||||
|
||||
# ключ: (user_id, тип) → thread_id
|
||||
user_topic_map: dict[tuple[int, str], int] = {}
|
||||
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 *
|
||||
35
bot/filters/callback.py
Normal file
35
bot/filters/callback.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("CallbackDataStartsWith", "CallbackStartsWith")
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
class CallbackStartsWith(BaseFilter):
|
||||
"""
|
||||
Фильтр для callback_data, которое начинается с команды из списка.
|
||||
Игнорирует регистр.
|
||||
"""
|
||||
def __init__(self, commands: list[str]):
|
||||
self.commands = [cmd.casefold() for cmd in commands]
|
||||
|
||||
async def __call__(self, callback: CallbackQuery) -> bool:
|
||||
data = callback.data.casefold() if callback.data else ""
|
||||
return any(data.startswith(cmd) for cmd in self.commands)
|
||||
152
bot/filters/chat_rights.py
Normal file
152
bot/filters/chat_rights.py
Normal file
@@ -0,0 +1,152 @@
|
||||
from typing import Any
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, ResultChatMemberUnion, CallbackQuery
|
||||
|
||||
from configs import ImportantID
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("IsChatCreator", "IsAdmin", "IsModerator", "IsOwner",)
|
||||
|
||||
|
||||
class IsOwner(BaseFilter):
|
||||
"""
|
||||
Фильтр для проверки, является ли пользователь владельцем бота.
|
||||
|
||||
Args:
|
||||
send_error_message (bool): Если True, при попытке не- владельца выполнить команду,
|
||||
бот отправит сообщение об ошибке.
|
||||
|
||||
Returns:
|
||||
bool | dict[str, Any]:
|
||||
- False, если пользователь не владелец и send_error_message=False
|
||||
- True, если пользователь является владельцем
|
||||
- dict с информацией о пользователе, если send_error_message=True
|
||||
|
||||
Example:
|
||||
@router.message(IsOwner())
|
||||
async def cmd_handler(message: Message):
|
||||
...
|
||||
|
||||
@router.message(IsOwner(send_error_message=True))
|
||||
async def admin_only(message: Message):
|
||||
...
|
||||
"""
|
||||
|
||||
def __init__(self, send_error_message: bool = False) -> None:
|
||||
"""
|
||||
Инициализация фильтра.
|
||||
|
||||
Args:
|
||||
send_error_message: Нужно ли отправлять сообщение при запрещенном доступе
|
||||
"""
|
||||
self.send_error_message: bool = send_error_message
|
||||
|
||||
async def __call__(self, update: Message | CallbackQuery, bot: Bot) -> bool | dict[str, Any]:
|
||||
"""
|
||||
Проверяет, является ли пользователь владельцем.
|
||||
|
||||
Args:
|
||||
update: Объект Message или CallbackQuery
|
||||
bot: Экземпляр бота (не используется, но требуется сигнатурой)
|
||||
|
||||
Returns:
|
||||
bool | dict[str, Any]: Результат фильтра. Если пользователь владелец,
|
||||
возвращается True или dict с info. Иначе False
|
||||
"""
|
||||
if not update.from_user:
|
||||
# Без from_user невозможно определить владельца
|
||||
return False
|
||||
|
||||
user_id: int = update.from_user.id
|
||||
is_owner: bool = user_id in ImportantID.OWNERS_ID
|
||||
|
||||
if not is_owner and self.send_error_message:
|
||||
# Отправляем предупреждение о доступе
|
||||
if isinstance(update, Message):
|
||||
await update.answer(text="⛔ Эта команда доступна только владельцу бота!")
|
||||
elif isinstance(update, CallbackQuery):
|
||||
await update.answer(text="⛔ Доступно только владельцу бота!", show_alert=True)
|
||||
return False
|
||||
|
||||
# Если пользователь владелец — возвращаем словарь с дополнительной информацией
|
||||
if is_owner:
|
||||
return {
|
||||
"is_owner": True,
|
||||
"user_id": user_id,
|
||||
"owner_ids": ImportantID.OWNERS_ID
|
||||
}
|
||||
|
||||
# Если не владелец и send_error_message=False
|
||||
return False
|
||||
|
||||
|
||||
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
|
||||
33
bot/filters/chat_type.py
Normal file
33
bot/filters/chat_type.py
Normal file
@@ -0,0 +1,33 @@
|
||||
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"}
|
||||
71
bot/filters/message_content.py
Normal file
71
bot/filters/message_content.py
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
41
bot/filters/subscrided.py
Normal file
41
bot/filters/subscrided.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from typing import Union
|
||||
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
|
||||
from aiogram.filters import BaseFilter
|
||||
from aiogram.types import Message, ResultChatMemberUnion
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__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
|
||||
21
bot/handlers/__init__.py
Normal file
21
bot/handlers/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .commands import router as cmd_routers
|
||||
from .messages import router as messages_routers
|
||||
from .form_utils import router as form_routers
|
||||
from .union_utills import router as union_routers
|
||||
from .custom import router as custom_routers
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
custom_routers,
|
||||
#cmd_routers,
|
||||
|
||||
#messages_routers,
|
||||
#form_routers,
|
||||
#union_routers,
|
||||
)
|
||||
21
bot/handlers/commands/__init__.py
Normal file
21
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .admins import router as admin_cmd_router
|
||||
from .special import router as special_cmd_router
|
||||
from .users import router as users_cmd_router
|
||||
from .users.cancel_cmd import router as cancel_cmd_router
|
||||
from .settings import router as settings_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
cancel_cmd_router,
|
||||
settings_cmd_router,
|
||||
admin_cmd_router,
|
||||
users_cmd_router,
|
||||
special_cmd_router,
|
||||
|
||||
)
|
||||
18
bot/handlers/commands/admins/__init__.py
Normal file
18
bot/handlers/commands/admins/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .ban_cmd import router as ban_cmd_router
|
||||
from .all_cmd import router as all_cmd_router
|
||||
from .pin_cmd import router as pin_cmd_router
|
||||
from .kick_cmd import router as kick_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
router.include_routers(
|
||||
ban_cmd_router,
|
||||
kick_cmd_router,
|
||||
pin_cmd_router,
|
||||
all_cmd_router,
|
||||
|
||||
)
|
||||
80
bot/handlers/commands/admins/all_cmd.py
Normal file
80
bot/handlers/commands/admins/all_cmd.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from asyncio import create_task
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from bot.core.bots import bot, BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.utils import status_clear, auto_delete_message, hidden_admins_message
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
# Ключ для команды
|
||||
CMD: str = "all"
|
||||
# Инициализация роутера
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(
|
||||
F.text.lower().regexp(rf"^({'|'.join(COMMANDS[CMD])})\s?.*"), # ловим текст без префикса
|
||||
F.chat.type.in_({"supergroup", "group"}),
|
||||
IsOwner()
|
||||
)
|
||||
@router.message(
|
||||
Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True),
|
||||
IsOwner()
|
||||
)
|
||||
async def notify_all_text(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /all, /call и текстовых эквивалентов типа "Калл Привет всем".
|
||||
|
||||
Функционал:
|
||||
1. Считывает весь текст после команды.
|
||||
2. Формирует скрытое сообщение для администраторов.
|
||||
3. Отправляет сообщение в чат.
|
||||
4. Автоматически удаляет сообщение через неделю.
|
||||
5. Пытается закрепить сообщение в чате.
|
||||
|
||||
Args:
|
||||
message (Message): Объект входящего сообщения.
|
||||
state (FSMContext): Контекст FSM, используется для очистки состояния.
|
||||
"""
|
||||
# Очистка состояния FSM перед выполнением команды
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Извлечение текста после команды
|
||||
parts: list[str] = message.text.split(" ", 1)
|
||||
custom_text: str = parts[1] if len(parts) > 1 else "⚡ Внимание всем!"
|
||||
|
||||
# Формирование скрытого текста для администраторов
|
||||
hidden_text: str = await hidden_admins_message(message=message, text=custom_text)
|
||||
|
||||
# Отправка сообщения в чат
|
||||
sent_message: Message = await message.answer(hidden_text)
|
||||
|
||||
# Запуск асинхронной задачи по удалению сообщения через 7 дней
|
||||
create_task(
|
||||
auto_delete_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
delay=604800 # 7 дней в секундах
|
||||
)
|
||||
)
|
||||
|
||||
# Попытка закрепить сообщение и удалить "системное" сообщение о закреплении
|
||||
try:
|
||||
await bot.pin_chat_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
disable_notification=False
|
||||
)
|
||||
# Иногда Telegram создает дополнительное уведомление при закреплении
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=sent_message.message_id + 1)
|
||||
logger.debug(f"[ALL] Сообщение закреплено: {custom_text}")
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"[ALL] Ошибка закрепления сообщения: {e}")
|
||||
258
bot/handlers/commands/admins/ban_cmd.py
Normal file
258
bot/handlers/commands/admins/ban_cmd.py
Normal file
@@ -0,0 +1,258 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, User
|
||||
from html import escape
|
||||
|
||||
from bot.filters import IsAdmin
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
from database import db
|
||||
|
||||
# Настройки роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
from middleware import logger
|
||||
|
||||
CMD: str = "ban"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS[CMD], ignore_case=True), IsAdmin())
|
||||
async def ban_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /ban для блокировки пользователей.
|
||||
Использование: /ban <user_id> или ответ на сообщение пользователя + /ban
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
try:
|
||||
# Проверяем есть ли ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
# Бан по ответу на сообщение
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
if not target_user:
|
||||
await message.answer("❌ Не удалось определить пользователя")
|
||||
return
|
||||
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
|
||||
# Проверяем, не пытаемся ли забанить бота
|
||||
if target_user_id == message.bot.id:
|
||||
await message.answer("❌ Нельзя заблокировать бота!")
|
||||
return
|
||||
|
||||
# Баним пользователя
|
||||
success: bool = await _ban_user(target_user_id, target_username, message)
|
||||
|
||||
if success:
|
||||
safe_username: str = escape(target_username)
|
||||
response_text = f"✅ Пользователь {safe_username} (ID: {target_user_id}) заблокирован!"
|
||||
|
||||
# Пытаемся забанить в чате (если команда вызвана в группе/чате)
|
||||
if message.chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
await message.bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user_id
|
||||
)
|
||||
response_text += "\n🚫 Пользователь исключен из чата."
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось исключить пользователя из чата: {e}")
|
||||
response_text += "\n⚠️ Не удалось исключить пользователя из чата."
|
||||
|
||||
await message.answer(
|
||||
text=response_text,
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Не удалось заблокировать пользователя")
|
||||
|
||||
else:
|
||||
# Бан по ID пользователя
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /ban\n"
|
||||
"• Или укажите ID: /ban <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
|
||||
# Проверяем, не пытаемся ли забанить бота
|
||||
if target_user_id == message.bot.id:
|
||||
await message.answer("❌ Нельзя заблокировать бота!")
|
||||
return
|
||||
|
||||
success: bool = await _ban_user(target_user_id, f"ID{target_user_id}", message)
|
||||
|
||||
if success:
|
||||
response_text = f"✅ Пользователь (ID: {target_user_id}) заблокирован!"
|
||||
|
||||
# Пытаемся забанить в чате
|
||||
if message.chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
await message.bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user_id
|
||||
)
|
||||
response_text += "\n🚫 Пользователь исключен из чата."
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось исключить пользователя из чата: {e}")
|
||||
response_text += "\n⚠️ Не удалось исключить пользователя из чата."
|
||||
|
||||
await message.answer(
|
||||
text=response_text,
|
||||
parse_mode=None
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Пользователь не найден или уже заблокирован")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в команде /ban: {e}")
|
||||
await message.answer(
|
||||
"⚠️ Произошла непредвиденная ошибка при выполнении команды.\n"
|
||||
"Попробуйте повторить действие позже или нажмите /start"
|
||||
)
|
||||
|
||||
|
||||
async def _ban_user(user_id: int, username: str, message: Message) -> bool:
|
||||
"""
|
||||
Внутренняя функция для блокировки пользователя.
|
||||
"""
|
||||
try:
|
||||
# Сначала проверяем существует ли пользователь
|
||||
user: User | None = await db.get_user(user_id)
|
||||
|
||||
if not user:
|
||||
# Если пользователя нет - создаем его забаненным
|
||||
await db.add_user(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
full_name=username
|
||||
)
|
||||
|
||||
# Баним пользователя
|
||||
await db.ban_user(user_id)
|
||||
|
||||
# Логируем действие
|
||||
admin_username = message.from_user.username or message.from_user.full_name or f"ID{message.from_user.id}"
|
||||
logger.info(f"🛑 Админ @{admin_username} заблокировал пользователя @{username} (ID: {user_id})")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при блокировке пользователя {user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("unban", ignore_case=True), IsAdmin())
|
||||
async def unban_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /unban для разблокировки пользователей.
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
try:
|
||||
if message.reply_to_message:
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
if not target_user:
|
||||
await message.answer("❌ Не удалось определить пользователя")
|
||||
return
|
||||
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
else:
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /unban\n"
|
||||
"• Или укажите ID: /unban <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
target_username: str = f"ID{target_user_id}"
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
return
|
||||
|
||||
# Разбаниваем пользователя
|
||||
await db.unban_user(target_user_id)
|
||||
|
||||
# Логируем действие
|
||||
admin_username: str = message.from_user.username or message.from_user.full_name or f"ID{message.from_user.id}"
|
||||
logger.info(f"🔓 Админ @{admin_username} разблокировал пользователя @{target_username} (ID: {target_user_id})")
|
||||
|
||||
# Экранируем специальные символы
|
||||
safe_username: str = escape(target_username)
|
||||
|
||||
response_text = f"✅ Пользователь {safe_username} (ID: {target_user_id}) разблокирован!"
|
||||
|
||||
# Пытаемся разбанить в чате
|
||||
if message.chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
await message.bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user_id
|
||||
)
|
||||
response_text += "\n👥 Пользователь может вернуться в чат."
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось разблокировать пользователя в чате: {e}")
|
||||
|
||||
await message.answer(
|
||||
text=response_text,
|
||||
parse_mode=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при разблокировке пользователя: {e}")
|
||||
await message.answer("❌ Не удалось разблокировать пользователя")
|
||||
|
||||
|
||||
@router.message(Command("banned_list", ignore_case=True), IsAdmin())
|
||||
async def banned_list_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /banned_list для просмотра списка забаненных пользователей.
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
try:
|
||||
# Получаем всех пользователей включая забаненных
|
||||
all_users: list[User] = await db.get_all_users(include_banned=True)
|
||||
|
||||
# Фильтруем только забаненных
|
||||
banned_users: list[User] = [user for user in all_users if getattr(user, 'status', None) == "banned"]
|
||||
|
||||
if not banned_users:
|
||||
await message.answer("📭 Список забаненных пользователей пуст")
|
||||
return
|
||||
|
||||
# Формируем сообщение со списком
|
||||
banned_list: str = "🚫 Заблокированные пользователи:\n\n"
|
||||
|
||||
for user in banned_users[:50]: # Ограничиваем вывод
|
||||
username: str = f"@{user.username}" if getattr(user, 'username', None) else getattr(user, 'full_name',
|
||||
'Неизвестно')
|
||||
# Экранируем специальные символы
|
||||
safe_username = escape(username)
|
||||
user_id = getattr(user, 'id', 'N/A')
|
||||
banned_list += f"• {safe_username} (ID: {user_id})\n"
|
||||
|
||||
if len(banned_users) > 50:
|
||||
banned_list += f"\n... и еще {len(banned_users) - 50} пользователей"
|
||||
|
||||
await message.answer(banned_list, parse_mode=None)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при получении списка забаненных: {e}")
|
||||
await message.answer("❌ Не удалось получить список забаненных пользователей")
|
||||
278
bot/handlers/commands/admins/kick_cmd.py
Normal file
278
bot/handlers/commands/admins/kick_cmd.py
Normal file
@@ -0,0 +1,278 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, User
|
||||
from html import escape
|
||||
|
||||
from bot import bot
|
||||
from bot.filters import IsAdmin
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
|
||||
# Настройки роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
from middleware import logger
|
||||
|
||||
CMD: str = "kick"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS[CMD], ignore_case=True), IsAdmin())
|
||||
async def kick_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /kick для кика пользователей из чата.
|
||||
Использование: /kick <user_id> или ответ на сообщение пользователя + /kick
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Проверяем, что команда используется в группе/супергруппе
|
||||
if message.chat.type not in ["group", "supergroup"]:
|
||||
await message.answer("❌ Эта команда работает только в группах и супергруппах!")
|
||||
return
|
||||
|
||||
# Проверяем есть ли ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
# Кик по ответу на сообщение
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
|
||||
# Кикаем пользователя
|
||||
success: bool = await _kick_user(target_user_id, target_username, message)
|
||||
|
||||
if success:
|
||||
safe_username: str = escape(target_username)
|
||||
await message.answer(
|
||||
text=f"👢 Пользователь {safe_username} (ID: {target_user_id}) кикнут из чата!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Не удалось кикнуть пользователя")
|
||||
|
||||
else:
|
||||
# Кик по ID пользователя
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /kick\n"
|
||||
"• Или укажите ID: /kick <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
success: bool = await _kick_user(target_user_id, f"ID{target_user_id}", message)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
text=f"👢 Пользователь (ID: {target_user_id}) кикнут из чата!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Пользователь не найден или не удалось кикнуть")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
|
||||
|
||||
async def _kick_user(user_id: int, username: str, message: Message) -> bool:
|
||||
"""
|
||||
Внутренняя функция для кика пользователя из чата.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя для кика
|
||||
username: Имя пользователя для логов
|
||||
message: Объект сообщения для контекста
|
||||
|
||||
Returns:
|
||||
bool: Успешно ли кикнут пользователь
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что бот имеет права администратора в чате
|
||||
bot_member = await bot.get_chat_member(message.chat.id, bot.id)
|
||||
if not bot_member.can_restrict_members:
|
||||
await message.answer("❌ У меня нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Проверяем, что целевой пользователь не является администратором/владельцем
|
||||
target_member = await bot.get_chat_member(message.chat.id, user_id)
|
||||
if target_member.status in ["creator", "administrator"]:
|
||||
await message.answer("❌ Нельзя кикнуть администратора или создателя чата!")
|
||||
return False
|
||||
|
||||
# Проверяем, что отправитель команды имеет права администратора
|
||||
admin_member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
if admin_member.status not in ["creator", "administrator"]:
|
||||
await message.answer("❌ У вас нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Кикаем пользователя из чата
|
||||
await bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id,
|
||||
revoke_messages=False # Не удаляем сообщения пользователя
|
||||
)
|
||||
|
||||
# Сразу разбаниваем, чтобы пользователь мог вернуться по приглашению
|
||||
await bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Логируем действие
|
||||
admin_username = message.from_user.username or message.from_user.full_name
|
||||
logger.info(
|
||||
f"👢 Админ @{admin_username} кикнул пользователя @{username} (ID: {user_id}) из чата {message.chat.title}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при кике пользователя {user_id}: {e}")
|
||||
await message.answer(f"❌ Ошибка при кике пользователя: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("kick_ban", ignore_case=True), IsAdmin())
|
||||
async def kick_ban_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /kick_ban для кика пользователя с удалением сообщений.
|
||||
Использование: /kick_ban <user_id> или ответ на сообщение пользователя + /kick_ban
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Проверяем, что команда используется в группе/супергруппе
|
||||
if message.chat.type not in ["group", "supergroup"]:
|
||||
await message.answer("❌ Эта команда работает только в группах и супергруппах!")
|
||||
return
|
||||
|
||||
# Проверяем есть ли ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
# Кик по ответу на сообщение
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
|
||||
# Кикаем пользователя с удалением сообщений
|
||||
success: bool = await _kick_ban_user(target_user_id, target_username, message)
|
||||
|
||||
if success:
|
||||
safe_username: str = escape(target_username)
|
||||
await message.answer(
|
||||
text=f"💥 Пользователь {safe_username} (ID: {target_user_id}) кикнут с удалением сообщений!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Не удалось кикнуть пользователя")
|
||||
|
||||
else:
|
||||
# Кик по ID пользователя
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /kick_ban\n"
|
||||
"• Или укажите ID: /kick_ban <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
success: bool = await _kick_ban_user(target_user_id, f"ID{target_user_id}", message)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
text=f"💥 Пользователь (ID: {target_user_id}) кикнут с удалением сообщений!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Пользователь не найден или не удалось кикнуть")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
|
||||
|
||||
async def _kick_ban_user(user_id: int, username: str, message: Message) -> bool:
|
||||
"""
|
||||
Внутренняя функция для кика пользователя с удалением сообщений.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя для кика
|
||||
username: Имя пользователя для логов
|
||||
message: Объект сообщения для контекста
|
||||
|
||||
Returns:
|
||||
bool: Успешно ли кикнут пользователь
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что бот имеет права администратора в чате
|
||||
bot_member = await bot.get_chat_member(message.chat.id, bot.id)
|
||||
if not bot_member.can_restrict_members:
|
||||
await message.answer("❌ У меня нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Проверяем, что целевой пользователь не является администратором/владельцем
|
||||
target_member = await bot.get_chat_member(message.chat.id, user_id)
|
||||
if target_member.status in ["creator", "administrator"]:
|
||||
await message.answer("❌ Нельзя кикнуть администратора или создателя чата!")
|
||||
return False
|
||||
|
||||
# Проверяем, что отправитель команды имеет права администратора
|
||||
admin_member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
if admin_member.status not in ["creator", "administrator"]:
|
||||
await message.answer("❌ У вас нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Кикаем пользователя из чата с удалением сообщений
|
||||
await bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id,
|
||||
revoke_messages=True # Удаляем сообщения пользователя
|
||||
)
|
||||
|
||||
# Сразу разбаниваем, чтобы пользователь мог вернуться по приглашению
|
||||
await bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Логируем действие
|
||||
admin_username = message.from_user.username or message.from_user.full_name
|
||||
logger.info(
|
||||
f"💥 Админ @{admin_username} кикнул пользователя @{username} (ID: {user_id}) из чата {message.chat.title} с удалением сообщений")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при кике пользователя {user_id} с удалением сообщений: {e}")
|
||||
await message.answer(f"❌ Ошибка при кике пользователя: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("kick_list", ignore_case=True), IsAdmin())
|
||||
async def kick_help_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /kick_list для показа справки по командам кика.
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
help_text = """
|
||||
🤖 **Команды модерации:**
|
||||
|
||||
**👢 /kick** - Кикнуть пользователя (может вернуться по приглашению)
|
||||
• Ответьте на сообщение пользователя с командой /kick
|
||||
• Или используйте: /kick <user_id>
|
||||
|
||||
**💥 /kick_ban** - Кикнуть пользователя с удалением сообщений
|
||||
• Ответьте на сообщение пользователя с командой /kick_ban
|
||||
• Или используйте: /kick_ban <user_id>
|
||||
|
||||
**🚫 /ban** - Полностью забанить пользователя
|
||||
**🔓 /unban** - Разбанить пользователя
|
||||
**📋 /banned_list** - Список забаненных
|
||||
|
||||
⚠️ *Команды работают только в группах и требуют прав администратора*
|
||||
"""
|
||||
|
||||
await message.answer(help_text, parse_mode=None)
|
||||
0
bot/handlers/commands/admins/mute_cmd.py
Normal file
0
bot/handlers/commands/admins/mute_cmd.py
Normal file
55
bot/handlers/commands/admins/pin_cmd.py
Normal file
55
bot/handlers/commands/admins/pin_cmd.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from asyncio import create_task
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from bot.core.bots import BotInfo, bot
|
||||
from bot.filters import IsOwner
|
||||
from bot.templates import msg
|
||||
from bot.utils import status_clear
|
||||
from bot.utils.auto_delete import auto_delete_message
|
||||
from configs import COMMANDS
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "pin".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def pin_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /pin для закрепления последнего сообщения или ответа.
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Если есть reply → закрепляем его, иначе закрепляем саму команду
|
||||
target = message.reply_to_message or message
|
||||
|
||||
await bot.pin_chat_message(chat_id=message.chat.id,
|
||||
message_id=target.message_id,
|
||||
disable_notification=False)
|
||||
|
||||
# Автоудаление через 7 суток
|
||||
create_task(auto_delete_message(chat_id=message.chat.id,
|
||||
message_id=target.message_id,
|
||||
delay=604800))
|
||||
|
||||
await msg(message=message, text="✅ Сообщение успешно закреплено")
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold().isin(COMMANDS[CMD]), IsOwner())
|
||||
async def pin_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик кнопки с callback_data="pin".
|
||||
"""
|
||||
await status_clear(message=callback.message, state=state)
|
||||
|
||||
await bot.pin_chat_message(chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id,
|
||||
disable_notification=False)
|
||||
|
||||
create_task(auto_delete_message(chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id,
|
||||
delay=604800))
|
||||
|
||||
await callback.answer("✅ Сообщение закреплено")
|
||||
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)
|
||||
0
bot/handlers/commands/admins/varn_cmd.py
Normal file
0
bot/handlers/commands/admins/varn_cmd.py
Normal file
19
bot/handlers/commands/settings/__init__.py
Normal file
19
bot/handlers/commands/settings/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .set_description_cmd import router as set_description_cmd_router
|
||||
from .set_name_cmd import router as set_name_cmd_router
|
||||
from .set_widget_cmd import router as set_widget_cmd_router
|
||||
from .settings_cmd import router as settings_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
settings_cmd_router,
|
||||
set_name_cmd_router,
|
||||
set_description_cmd_router,
|
||||
set_widget_cmd_router,
|
||||
)
|
||||
167
bot/handlers/commands/settings/set_description_cmd.py
Normal file
167
bot/handlers/commands/settings/set_description_cmd.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
|
||||
from bot.templates import msg
|
||||
from bot.utils import format_retry_time, status_clear
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
# Название команды
|
||||
CMD: str = "set_description".lower()
|
||||
|
||||
# Роутер для обработки команды /set_description
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
class SetBotDescriptionForm(StatesGroup):
|
||||
"""Состояния FSM для изменения короткого описания бота."""
|
||||
new_description: State = State()
|
||||
|
||||
|
||||
async def handle_set_bot_description(
|
||||
description: str,
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Установка короткого описания (short description) бота с обработкой FSM и ошибок API.
|
||||
|
||||
Args:
|
||||
description (str): Новый текст описания (до 120 символов).
|
||||
message (Message | CallbackQuery): Сообщение или callback-запрос.
|
||||
state (FSMContext): Контекст FSM.
|
||||
bot (Bot): Экземпляр бота.
|
||||
"""
|
||||
# Проверка ограничения Telegram
|
||||
if len(description) > 120:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Короткое описание бота должно быть не более 120 символов. Текущая длина: {length}").format(
|
||||
length=len(description)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Установка нового короткого описания
|
||||
await bot.set_my_short_description(short_description=description)
|
||||
|
||||
# Сохраняем текущее значение в BotInfo
|
||||
BotInfo.short_description = description
|
||||
|
||||
# Сбрасываем состояние FSM
|
||||
await state.clear()
|
||||
|
||||
# Отправляем сообщение об успехе
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("✅ Короткое описание бота успешно изменено на: <b>{description}</b>").format(
|
||||
description=description
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
logger.info(f"Короткое описание бота изменено на: {description}")
|
||||
|
||||
except TelegramRetryAfter as e:
|
||||
retry_text: str = format_retry_time(e.retry_after)
|
||||
logger.warning(f"Превышен лимит запросов при смене short description. Попробуйте через {retry_text}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("⚠️ Слишком частая смена короткого описания!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
logger.error(f"Ошибка Telegram API при изменении короткого описания: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Ошибка Telegram API при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при изменении короткого описания: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Непредвиденная ошибка при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot,
|
||||
command: CommandObject | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Обработчик команды /set_description для короткого описания.
|
||||
|
||||
Поддерживает:
|
||||
1. Немедленное изменение через аргумент (/set_description TEXT).
|
||||
2. Callback-запрос.
|
||||
3. FSM-ввод.
|
||||
"""
|
||||
current_description: str = BotInfo.description
|
||||
|
||||
# Вариант 1: если пользователь передал аргумент к команде
|
||||
if command and command.args:
|
||||
description: str = command.args.strip()
|
||||
if len(description) > 120:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Короткое описание не должно превышать 120 символов. Текущая длина: {length}").format(
|
||||
length=len(description)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
await handle_set_bot_description(description, message, state, bot)
|
||||
return
|
||||
|
||||
# Вариант 2: без аргумента → включаем FSM
|
||||
await status_clear(message=message, state=state)
|
||||
text: str = _(
|
||||
"📝 <b>Смена короткого описания бота</b>\n\n"
|
||||
"Текущее короткое описание: <i>{current}</i>\n\n"
|
||||
"Введите новое короткое описание (максимум 120 символов):"
|
||||
).format(current=current_description)
|
||||
|
||||
await msg(message=message, text=text, markup=settings_keyboard())
|
||||
await state.set_state(SetBotDescriptionForm.new_description)
|
||||
|
||||
|
||||
@router.message(SetBotDescriptionForm.new_description, IsOwner())
|
||||
async def process_new_bot_description(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Обработка ввода нового короткого описания через FSM.
|
||||
"""
|
||||
description: str = message.text.strip()
|
||||
|
||||
if not description:
|
||||
await message.answer(_("❌ Пожалуйста, введите корректное короткое описание."))
|
||||
return
|
||||
|
||||
await handle_set_bot_description(description, message, state, bot)
|
||||
151
bot/handlers/commands/settings/set_name_cmd.py
Normal file
151
bot/handlers/commands/settings/set_name_cmd.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
|
||||
from bot.templates import msg
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "set_name".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
class SetNameForm(StatesGroup):
|
||||
new_name: State = State()
|
||||
|
||||
|
||||
def format_retry_time(retry_after: int) -> str:
|
||||
"""Форматирование времени повторной попытки в читаемом виде"""
|
||||
hours, remainder = divmod(retry_after, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours} часов, {minutes} минут, {seconds} секунд"
|
||||
elif minutes > 0:
|
||||
return f"{minutes} минут, {seconds} секунд"
|
||||
else:
|
||||
return f"{seconds} секунд"
|
||||
|
||||
|
||||
async def handle_set_name(
|
||||
new_name: str,
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Установка имени бота с проверкой длины, обработкой перегрузки и логированием
|
||||
"""
|
||||
if len(new_name) > 64:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Имя бота должно быть не более 64 символов. Текущая длина: {length}").format(
|
||||
length=len(new_name)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.set_my_name(new_name)
|
||||
BotInfo.first_name = new_name
|
||||
await state.clear()
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("✅ Имя бота успешно изменено на: <b>{new_name}</b>").format(new_name=new_name),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
logger.info(f"Имя бота изменено на: {new_name}")
|
||||
|
||||
except TelegramRetryAfter as e:
|
||||
retry_text: str = format_retry_time(e.retry_after)
|
||||
logger.warning(f"Превышен контроль перегрузки при смене имени. Попробуйте через {retry_text}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("⚠️ Слишком частая смена имени!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
logger.error(f"Ошибка Telegram API при изменении имени: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Ошибка Telegram API: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при изменении имени: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Непредвиденная ошибка: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot,
|
||||
command: CommandObject | None = None
|
||||
):
|
||||
"""
|
||||
Обработчик команды /set_name с поддержкой:
|
||||
1. Immediate установки через аргумент команды
|
||||
2. Callback query
|
||||
3. FSM ввод
|
||||
"""
|
||||
current_name = getattr(BotInfo, "first_name", "") or _("Не установлено")
|
||||
|
||||
# Immediate установка через аргумент команды
|
||||
if command and command.args:
|
||||
new_name = command.args.strip()
|
||||
if len(new_name) > 64:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Имя не должно превышать 64 символа. Текущая длина: {length}").format(
|
||||
length=len(new_name)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
await handle_set_name(new_name, message, state, bot)
|
||||
return
|
||||
|
||||
# Для callback query или пустой команды — показываем текущее имя и запускаем FSM
|
||||
await state.clear()
|
||||
if isinstance(message, CallbackQuery):
|
||||
await message.answer()
|
||||
text: str = _(
|
||||
"🤖 <b>Смена имени бота</b>\n\n"
|
||||
"Текущее имя: <i>{current}</i>\n\n"
|
||||
"Пожалуйста, введите новое имя для бота (максимум 64 символа):"
|
||||
).format(current=current_name)
|
||||
await msg(message=message, text=text, markup=settings_keyboard())
|
||||
await state.set_state(SetNameForm.new_name)
|
||||
|
||||
|
||||
@router.message(SetNameForm.new_name, IsOwner())
|
||||
async def process_new_name(message: Message, state: FSMContext, bot: Bot):
|
||||
"""
|
||||
Обработка ввода нового имени через FSM
|
||||
"""
|
||||
new_name: str = message.text.strip()
|
||||
|
||||
if not new_name:
|
||||
await message.answer(_("❌ Пожалуйста, введите корректное имя."))
|
||||
return
|
||||
|
||||
await handle_set_name(new_name, message, state, bot)
|
||||
168
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
168
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
@@ -0,0 +1,168 @@
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
|
||||
from bot.templates import msg
|
||||
from bot.utils import format_retry_time, status_clear
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "set_widget".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
class SetWidgetForm(StatesGroup):
|
||||
"""Состояния FSM для изменения виджета (описания бота)."""
|
||||
new_widget: State = State()
|
||||
|
||||
|
||||
async def handle_set_widget(
|
||||
new_widget: str,
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Устанавливает новое значение виджета (описания бота).
|
||||
|
||||
Args:
|
||||
new_widget (str): Новый текст виджета.
|
||||
message (Message | CallbackQuery): Объект сообщения или callback-запроса.
|
||||
state (FSMContext): Контекст состояния FSM.
|
||||
bot (Bot): Экземпляр текущего бота.
|
||||
"""
|
||||
# Проверка длины текста (Telegram API ограничивает description до 512 символов)
|
||||
if len(new_widget) > 512:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Виджет бота должен быть не более 512 символов. Текущая длина: {length}").format(
|
||||
length=len(new_widget)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Устанавливаем описание через Telegram API
|
||||
await bot.set_my_description(description=new_widget)
|
||||
|
||||
# Сохраняем в BotInfo для локального использования
|
||||
BotInfo.widget = new_widget
|
||||
|
||||
# Очищаем состояние FSM
|
||||
await state.clear()
|
||||
|
||||
# Отправляем уведомление пользователю
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("✅ Виджет бота успешно изменён на: <b>{new_widget}</b>").format(
|
||||
new_widget=new_widget
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
logger.info(f"Виджет бота изменён на: {new_widget}")
|
||||
|
||||
except TelegramRetryAfter as e:
|
||||
# Если запрос слишком частый
|
||||
retry_text: str = format_retry_time(e.retry_after)
|
||||
logger.warning(f"Превышен лимит запросов при смене виджета. Попробуйте через {retry_text}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("⚠️ Слишком частая смена виджета!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
# Ошибка Telegram API
|
||||
logger.error(f"Ошибка Telegram API при изменении виджета: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Ошибка Telegram API при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Непредвиденная ошибка
|
||||
logger.error(f"Непредвиденная ошибка при изменении виджета: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Непредвиденная ошибка при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot,
|
||||
command: CommandObject | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Обработчик команды /set_widget.
|
||||
|
||||
Поддерживает:
|
||||
1. Немедленное изменение через аргумент команды (/set_widget TEXT).
|
||||
2. Callback-запрос.
|
||||
3. FSM ввод.
|
||||
"""
|
||||
# Получаем текущее значение виджета
|
||||
current_widget: str = BotInfo.widget
|
||||
|
||||
# Вариант 1: пользователь ввёл аргумент сразу (/set_widget TEXT)
|
||||
if command and command.args:
|
||||
new_widget: str = command.args.strip()
|
||||
if len(new_widget) > 512:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Виджет не должен превышать 512 символов. Текущая длина: {length}").format(
|
||||
length=len(new_widget)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
await handle_set_widget(new_widget, message, state, bot)
|
||||
return
|
||||
|
||||
# Вариант 2: Callback query или пустая команда → запускаем FSM
|
||||
await status_clear(message=message, state=state)
|
||||
text: str = _(
|
||||
"📝 <b>Смена виджета бота</b>\n\n"
|
||||
"Текущий виджет: <i>{current}</i>\n\n"
|
||||
"Пожалуйста, введите новый виджет для бота (максимум 512 символов):"
|
||||
).format(current=current_widget)
|
||||
|
||||
await msg(message=message, text=text, markup=settings_keyboard())
|
||||
await state.set_state(SetWidgetForm.new_widget)
|
||||
|
||||
|
||||
@router.message(SetWidgetForm.new_widget, IsOwner())
|
||||
async def process_new_widget(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Обрабатывает ввод нового текста виджета через FSM.
|
||||
"""
|
||||
new_widget: str = message.text.strip()
|
||||
|
||||
# Проверяем, что пользователь что-то ввёл
|
||||
if not new_widget:
|
||||
await message.answer(_("❌ Пожалуйста, введите корректный виджет."))
|
||||
return
|
||||
|
||||
await handle_set_widget(new_widget, message, state, bot)
|
||||
48
bot/handlers/commands/settings/settings_cmd.py
Normal file
48
bot/handlers/commands/settings/settings_cmd.py
Normal file
@@ -0,0 +1,48 @@
|
||||
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.i18n import gettext as _
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.templates import msg
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router", "settings_keyboard",)
|
||||
CMD: str = "settings".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
def settings_keyboard() -> InlineKeyboardBuilder:
|
||||
"""Клавиатура настроек"""
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="🔙 Вернуться", callback_data="settings"))
|
||||
return ikb
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /settings"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Имя бота⚜️", callback_data='set_name'))
|
||||
ikb.row(InlineKeyboardButton(text="Описание бота📝", callback_data='set_description'))
|
||||
ikb.row(InlineKeyboardButton(text="Виджет🧩", callback_data='set_widget'))
|
||||
ikb.row(InlineKeyboardButton(text="Назад◀️", callback_data='menu'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _("""
|
||||
⚙️ Настройки
|
||||
"""
|
||||
).format(
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg(message=message, text=text, markup=ikb)
|
||||
9
bot/handlers/commands/special/__init__.py
Normal file
9
bot/handlers/commands/special/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from aiogram import Router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
# router.include_routers(
|
||||
# )
|
||||
22
bot/handlers/commands/users/__init__.py
Normal file
22
bot/handlers/commands/users/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from aiogram import Router
|
||||
|
||||
#from .active import router as active_cmd_router
|
||||
from .start_cmd import router as start_cmd_router
|
||||
#from .union_cmd import router as union_cmd_router
|
||||
from .new_cmd import router as new_cmd_router
|
||||
#from .create_cmd import router as create_cmd_router
|
||||
#from .anon import router as anon_router
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
start_cmd_router,
|
||||
#active_cmd_router,
|
||||
#union_cmd_router,
|
||||
new_cmd_router,
|
||||
#create_cmd_router,
|
||||
#anon_router,
|
||||
)
|
||||
42
bot/handlers/commands/users/active.py
Normal file
42
bot/handlers/commands/users/active.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils import status_clear
|
||||
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 status_clear(message=message, state=state)
|
||||
|
||||
# Получить статистику сообщений пользователя
|
||||
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, )
|
||||
117
bot/handlers/commands/users/anon.py
Normal file
117
bot/handlers/commands/users/anon.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from typing import Dict, Tuple
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from bot.utils import status_clear
|
||||
|
||||
# -------------------
|
||||
# Router
|
||||
# -------------------
|
||||
router: Router = Router(name="anon_router")
|
||||
|
||||
# -------------------
|
||||
# Конфигурация
|
||||
# -------------------
|
||||
# CHAT_ID в формате "-100000_29" -> chat_id + thread_id
|
||||
CHAT_ID: str = "-1003098225669_724"
|
||||
|
||||
def parse_chat_id(chat_id_str: str) -> Tuple[int, int]:
|
||||
chat_str, thread_str = chat_id_str.split("_")
|
||||
return int(chat_str), int(thread_str)
|
||||
|
||||
ADMIN_CHAT_ID, ADMIN_THREAD_ID = parse_chat_id(CHAT_ID)
|
||||
|
||||
# -------------------
|
||||
# FSM состояния
|
||||
# -------------------
|
||||
class AnonStates:
|
||||
USER_WAITING_TEXT = "user_waiting_text"
|
||||
ADMIN_WAITING_REPLY = "admin_waiting_reply"
|
||||
|
||||
# -------------------
|
||||
# Словари для отслеживания сообщений
|
||||
# -------------------
|
||||
# user_id -> message_id в админском топике
|
||||
user_to_admin_map: Dict[int, int] = {}
|
||||
# admin_message_id -> user_id
|
||||
admin_to_user_map: Dict[int, int] = {}
|
||||
|
||||
# -------------------
|
||||
# Команда /anon или callback
|
||||
# -------------------
|
||||
@router.callback_query(F.data.casefold() == "anon")
|
||||
@router.message(Command("anon"))
|
||||
async def anon_start(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Начало анонимного сообщения. Ждём текст пользователя."""
|
||||
await status_clear(message=message, state=state)
|
||||
await state.clear()
|
||||
await state.set_state(AnonStates.USER_WAITING_TEXT)
|
||||
|
||||
text = "Напишите сообщение, которое вы хотите отправить анонимно администраторам."
|
||||
if isinstance(message, Message):
|
||||
await message.reply(text)
|
||||
else:
|
||||
await message.message.answer(text)
|
||||
|
||||
# -------------------
|
||||
# Получение текста от пользователя
|
||||
# -------------------
|
||||
@router.message(F.text, F.state == AnonStates.USER_WAITING_TEXT)
|
||||
async def anon_send_text(message: Message, state: FSMContext) -> None:
|
||||
"""Пересылает текст пользователя в админский топик анонимно."""
|
||||
anon_text = message.text.strip()
|
||||
if not anon_text:
|
||||
await message.reply("Сообщение не может быть пустым. Попробуйте снова.")
|
||||
return
|
||||
|
||||
forwarded_text = f"Сообщение от [пользователя](tg://user?id={message.from_user.id}):\n{anon_text}"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Ответить", callback_data=f"anon_reply:{message.from_user.id}")]
|
||||
]
|
||||
)
|
||||
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ADMIN_CHAT_ID,
|
||||
message_thread_id=ADMIN_THREAD_ID,
|
||||
text=forwarded_text,
|
||||
parse_mode="Markdown",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
user_to_admin_map[message.from_user.id] = sent_msg.message_id
|
||||
admin_to_user_map[sent_msg.message_id] = message.from_user.id
|
||||
|
||||
await message.reply("Ваше сообщение отправлено анонимно администраторам.")
|
||||
await state.clear()
|
||||
|
||||
# -------------------
|
||||
# Кнопка "Ответить" админа
|
||||
# -------------------
|
||||
@router.callback_query(F.data.startswith("anon_reply:"))
|
||||
async def anon_admin_reply(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Начинаем сессию ответа админа пользователю."""
|
||||
user_id = int(callback.data.split(":")[1])
|
||||
await state.set_state(AnonStates.ADMIN_WAITING_REPLY)
|
||||
await state.update_data(reply_to_user=user_id)
|
||||
await callback.message.answer(f"Введите ответ для пользователя [id={user_id}]:")
|
||||
await callback.answer()
|
||||
|
||||
# -------------------
|
||||
# Текст ответа админа
|
||||
# -------------------
|
||||
@router.message(F.text, F.state == AnonStates.ADMIN_WAITING_REPLY)
|
||||
async def anon_send_admin_text(message: Message, state: FSMContext) -> None:
|
||||
"""Пересылает текст админа пользователю."""
|
||||
data = await state.get_data()
|
||||
reply_to_user = data.get("reply_to_user")
|
||||
|
||||
if reply_to_user:
|
||||
await message.bot.send_message(
|
||||
chat_id=reply_to_user,
|
||||
text=f"Ответ администратора:\n{message.text}"
|
||||
)
|
||||
await message.reply("Сообщение отправлено пользователю.")
|
||||
await state.clear()
|
||||
27
bot/handlers/commands/users/cancel_cmd.py
Normal file
27
bot/handlers/commands/users/cancel_cmd.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot import BotInfo
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "cancel".casefold()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
@router.message(F.text.casefold().in_(COMMANDS[CMD]))
|
||||
async def cancel_handler(message: Message, state: FSMContext, text: str = "❌ Отмена предыдущего действия!"):
|
||||
"""
|
||||
Позволяет пользователю отменить процесс смены описания
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
logger.info(text=text)
|
||||
|
||||
await message.answer(text)
|
||||
49
bot/handlers/commands/users/create_cmd.py
Normal file
49
bot/handlers/commands/users/create_cmd.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# bot/handlers/commands/create_cmd.py
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from bot.core import BotInfo
|
||||
from bot.states.anketa_states import StartForm
|
||||
from bot.templates import msg_photo
|
||||
from middleware import log
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name="create_cmd_router")
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data == "create")
|
||||
@router.message(Command('create','скуфеу', 'анкета', prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type='Start', text="использовал(а) команду /create")
|
||||
async def create_cmd(message: Message|CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /create.
|
||||
"""
|
||||
# Сбросим все состояния (отменим создание поста, если оно было)
|
||||
await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Правила❗️", url='https://teletype.in/@velli_arsaan/XxUiHcB4Puj'))
|
||||
ikb.row(InlineKeyboardButton(text="Назад↪️", callback_data='start'))
|
||||
|
||||
# Создание базовых переменных сообщения
|
||||
caption: str = f"""
|
||||
Если вы хотели бы вступить в наш проект, то напоминаю, что вам сначала нужно ознакомиться с <b>инфо-каналом</b>! При продолжении диалога вы автоматически подтверждаете то, что прочитали все правила и в курсе, что мы ролевой проект, не флуд.
|
||||
<blockquote>Чтобы вступить к вам мы просим вас заполнить небольшую анкету:
|
||||
1. <i>Желаемая роль</i>;
|
||||
2. <i>Кого бы вы хотели в соролы?</i>;
|
||||
3. <i>Кодовая фраза из наших правил</i>;</blockquote>
|
||||
[‼️] Оно состоит всего из 4 слов, которые разбросаны в верном порядке по статьям о правилах.
|
||||
"""
|
||||
# Установим состояние ожидания анкеты
|
||||
await state.set_state(StartForm.waiting_for_application)
|
||||
|
||||
# Обработчик ответа на сообщение
|
||||
await msg_photo(message=message, text=caption, file='assets/help.png', markup=ikb)
|
||||
|
||||
368
bot/handlers/commands/users/new_cmd.py
Normal file
368
bot/handlers/commands/users/new_cmd.py
Normal file
@@ -0,0 +1,368 @@
|
||||
from typing import Dict
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, InlineKeyboardButton, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS, ImportantID
|
||||
from middleware.loggers import log
|
||||
|
||||
# user_id -> thread_id (топик пользователя)
|
||||
user_topic_map: Dict[int, int] = {}
|
||||
# message_id в топике -> user_id
|
||||
topic_message_map: Dict[int, int] = {}
|
||||
|
||||
__all__ = ("router", "user_topic_map")
|
||||
CMD: str = "new"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
STATE_WAITING_REQUEST = "waiting_request"
|
||||
|
||||
|
||||
def has_active_topic(user_id: int) -> bool:
|
||||
"""Проверяет, есть ли у пользователя активный топик"""
|
||||
return user_id in user_topic_map
|
||||
|
||||
|
||||
async def send_topic_message(user_id: int, text: str, reply_markup=None):
|
||||
"""Отправляет сообщение в топик пользователя"""
|
||||
thread_id = user_topic_map.get(user_id)
|
||||
if not thread_id:
|
||||
return False
|
||||
|
||||
try:
|
||||
await BotInfo.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log(level='ERROR', log_type='TOPIC_SEND', text=f"Ошибка отправки в топик: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ===================== Продолжение диалога =====================
|
||||
@router.callback_query(F.data == "continue_dialog")
|
||||
async def continue_dialog_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик продолжения существующего диалога"""
|
||||
user_id = callback.from_user.id
|
||||
|
||||
if not has_active_topic(user_id):
|
||||
await callback.answer("❌ Активный диалог не найден", show_alert=True)
|
||||
return
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await callback.message.edit_text(
|
||||
text="💬 У вас уже есть активный диалог с поддержкой. Просто отправьте ваше сообщение (не через reply) и оно будет переслано администратору.",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ===================== Обработчик callback /new =====================
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@log(level='INFO', log_type=f"{CMD.upper()}_CBD", text=f"использовал команду /{CMD} через кнопку")
|
||||
async def new_cmd_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /new из callback кнопки"""
|
||||
user_id = callback.from_user.id
|
||||
|
||||
# Проверяем, есть ли уже активный топик
|
||||
if has_active_topic(user_id):
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
|
||||
ikb.row(InlineKeyboardButton(text="Создать новый📝", callback_data='force_new'))
|
||||
|
||||
await callback.message.edit_text(
|
||||
text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n"
|
||||
"• <b>Продолжить текущий</b> - чтобы писать в существующий диалог\n"
|
||||
"• <b>Создать новый</b> - если хотите начать новый запрос (старый диалог будет архивирован)",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
await status_clear(message=callback.message, state=state)
|
||||
await state.set_state(STATE_WAITING_REQUEST)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text="Отправьте свой запрос:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
except Exception:
|
||||
await callback.message.answer(
|
||||
text="Отправьте свой запрос:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ===================== Принудительное создание нового топика =====================
|
||||
@router.callback_query(F.data == "force_new")
|
||||
async def force_new_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Принудительное создание нового топика (при наличии активного)"""
|
||||
user_id = callback.from_user.id
|
||||
|
||||
# Уведомляем в старом топике о создании нового
|
||||
if has_active_topic(user_id):
|
||||
await send_topic_message(
|
||||
user_id,
|
||||
f"🔔 <b>Пользователь начал новый запрос</b>\n"
|
||||
f"Старый топик будет архивирован."
|
||||
)
|
||||
# Не удаляем старый топик из мапы сразу - он перезапишется при создании нового
|
||||
|
||||
await status_clear(message=callback.message, state=state)
|
||||
await state.set_state(STATE_WAITING_REQUEST)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await callback.message.edit_text(
|
||||
text="📝 <b>Создание нового запроса</b>\n\nОтправьте ваш запрос:",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ===================== Обработчик сообщения /new =====================
|
||||
@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: Message, state: FSMContext) -> None:
|
||||
"""Обработчик команды /new из текстового сообщения"""
|
||||
user_id = message.from_user.id
|
||||
|
||||
# Проверяем, есть ли уже активный топик
|
||||
if has_active_topic(user_id):
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
|
||||
|
||||
await message.answer(
|
||||
text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n"
|
||||
"Используйте кнопку ниже чтобы продолжить общение в существующем диалоге.",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
await status_clear(message=message, state=state)
|
||||
await state.set_state(STATE_WAITING_REQUEST)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await message.answer(
|
||||
text="Отправьте свой запрос:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
|
||||
|
||||
# ===================== Создание топика и отправка запроса =====================
|
||||
@router.message(StateFilter(STATE_WAITING_REQUEST))
|
||||
async def process_request(message: Message, state: FSMContext) -> None:
|
||||
"""Создание топика и отправка запроса пользователя"""
|
||||
text = message.text.strip()
|
||||
if not text:
|
||||
await message.reply("⚠️ Пожалуйста, отправьте непустое сообщение.")
|
||||
return
|
||||
|
||||
user = message.from_user
|
||||
|
||||
try:
|
||||
# Создаем новый топик для пользователя
|
||||
topic_name = f"👤 {user.full_name} (ID: {user.id})"
|
||||
topic_result = await message.bot.create_forum_topic(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
name=topic_name
|
||||
)
|
||||
|
||||
thread_id = topic_result.message_thread_id
|
||||
|
||||
# Отправляем сообщение пользователя в новый топик
|
||||
formatted_text = f"<b>📩 Сообщение от <a href='tg://user?id={user.id}'>{user.full_name}</a>:</b>\n{text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=formatted_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем сообщение с уведомлением (со звуком)
|
||||
await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text="🔔 <b>Новый запрос создан</b>\nАдминистратор уведомлен.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Сохраняем связь пользователя и топика
|
||||
user_topic_map[user.id] = thread_id
|
||||
topic_message_map[sent_msg.message_id] = user.id
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Перейти к диалогу💬", callback_data='continue_dialog'))
|
||||
ikb.row(InlineKeyboardButton(text="В меню↩️", callback_data='start'))
|
||||
|
||||
await message.answer(
|
||||
text="✅ <b>Запрос отправлен!</b>\n\n"
|
||||
"Администратор ответит в этом боте. Вы можете продолжить общение через меню.",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
await message.reply(f"⚠️ Не удалось создать запрос: {e}")
|
||||
|
||||
|
||||
# ===================== Пересылка сообщений пользователя в топик =====================
|
||||
@router.message(F.chat.type == "private", ~F.reply_to_message)
|
||||
async def forward_user_to_admin(message: Message) -> None:
|
||||
"""Пересылает сообщения пользователя в топик (если есть активный диалог)"""
|
||||
if message.from_user.is_bot:
|
||||
return
|
||||
|
||||
user_id = message.from_user.id
|
||||
|
||||
# Проверяем, есть ли активный топик
|
||||
if not has_active_topic(user_id):
|
||||
return # Нет активного топика - игнорируем
|
||||
|
||||
# Получаем топик пользователя
|
||||
thread_id = user_topic_map.get(user_id)
|
||||
if not thread_id:
|
||||
return
|
||||
|
||||
try:
|
||||
# Отправляем сообщение пользователя в топик
|
||||
if message.text:
|
||||
formatted_text = f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=formatted_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
elif message.photo:
|
||||
caption = f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
|
||||
sent_msg = await message.bot.send_photo(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
await message.answer("✅ Сообщение отправлено администратору")
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"⚠️ Не удалось отправить сообщение: {e}")
|
||||
|
||||
|
||||
# ===================== Пересылка ответов админа пользователю =====================
|
||||
@router.message(F.chat.id == ImportantID.SUPPORT_CHAT_ID, F.message_thread_id)
|
||||
async def forward_admin_to_user(message: Message) -> None:
|
||||
"""Пересылает сообщения админа из топика пользователю"""
|
||||
if message.from_user.is_bot:
|
||||
return
|
||||
|
||||
thread_id = message.message_thread_id
|
||||
|
||||
# Ищем пользователя по 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 # Не наш топик
|
||||
|
||||
try:
|
||||
# Пересылаем сообщение админа пользователю
|
||||
if message.text:
|
||||
text = f"<b>👨💼 Ответ администратора:</b>\n{message.html_text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
# Сохраняем связь для возможного ответа пользователя
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
elif message.photo:
|
||||
caption = f"<b>👨💼 Ответ администратора:</b>\n{message.html_text}" if message.caption else "<b>👨💼 Ответ администратора:</b>"
|
||||
await message.bot.send_photo(
|
||||
chat_id=user_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log(level='ERROR', log_type='FORWARD', text=f"Ошибка пересылки админ->пользователь: {e}")
|
||||
|
||||
|
||||
# ===================== Пересылка ответов пользователя в топик =====================
|
||||
@router.message(F.chat.type == "private", F.reply_to_message)
|
||||
async def forward_user_reply_to_admin(message: Message) -> None:
|
||||
"""Пересылает ответы пользователя (reply) в топик"""
|
||||
if message.from_user.is_bot:
|
||||
return
|
||||
|
||||
user_id = message.from_user.id
|
||||
reply_to_id = message.reply_to_message.message_id
|
||||
|
||||
# Проверяем, является ли это ответом на сообщение из топика
|
||||
original_user_id = topic_message_map.get(reply_to_id)
|
||||
if not original_user_id or original_user_id != user_id:
|
||||
return
|
||||
|
||||
# Получаем топик пользователя
|
||||
thread_id = user_topic_map.get(user_id)
|
||||
if not thread_id:
|
||||
await message.reply("⚠️ Не найден активный диалог. Используйте /new для нового запроса.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Отправляем ответ пользователя в топик
|
||||
if message.text:
|
||||
formatted_text = f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=formatted_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
elif message.photo:
|
||||
caption = f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
|
||||
sent_msg = await message.bot.send_photo(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
await message.reply("✅ Ответ отправлен администратору.")
|
||||
|
||||
except Exception as e:
|
||||
await message.reply(f"⚠️ Не удалось отправить ответ: {e}")
|
||||
68
bot/handlers/commands/users/start_cmd.py
Normal file
68
bot/handlers/commands/users/start_cmd.py
Normal file
@@ -0,0 +1,68 @@
|
||||
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.i18n import gettext as _
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.templates import msg_photo
|
||||
from configs import COMMANDS, RpValue
|
||||
from .new_cmd import user_topic_map # Импортируем мапу топиков из модуля new
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "start".casefold()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
async def start_cmd(update: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /start"""
|
||||
# Определяем тип update
|
||||
if isinstance(update, CallbackQuery):
|
||||
message = update.message
|
||||
callback = update
|
||||
else:
|
||||
message = update
|
||||
callback = None
|
||||
|
||||
# Проверяем, есть ли у пользователя активный топик
|
||||
user_id = update.from_user.id
|
||||
has_active_topic = user_id in user_topic_map
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Википедия🌐", url="https://t.me/PrimoWiki"))
|
||||
|
||||
if has_active_topic:
|
||||
# Если есть активный топик, показываем кнопку "Продолжить диалог"
|
||||
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
|
||||
else:
|
||||
# Если нет активного топика, показываем кнопку "Связаться"
|
||||
ikb.row(InlineKeyboardButton(text="Связаться👀", callback_data='new'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||
|
||||
Я ваш помощник по проекту — <b>PrimoWiki</b>!
|
||||
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||
|
||||
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||
"""
|
||||
).format(
|
||||
url=update.from_user.url,
|
||||
name=update.from_user.first_name,
|
||||
rp_name=RpValue.RP_NAME,
|
||||
)
|
||||
|
||||
# Добавляем информацию об активном диалоге, если есть
|
||||
if has_active_topic:
|
||||
text += "\n\n💬 <b>У вас есть активный диалог с поддержкой!</b>"
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||
|
||||
if callback:
|
||||
await callback.answer()
|
||||
58
bot/handlers/commands/users/union_cmd.py
Normal file
58
bot/handlers/commands/users/union_cmd.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# bot/handlers/commands/union_cmd.py
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from bot.core import BotInfo
|
||||
from bot.states.union_states import UnionStates
|
||||
from bot.templates import msg
|
||||
from middleware import log
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
|
||||
router: Router = Router(name="union_cmd_router")
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data == "union")
|
||||
@router.message(Command('union','гтшщт', 'союз', prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type='Start', text="использовал(а) команду /union")
|
||||
async def create_cmd(message: Message|CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /union.
|
||||
"""
|
||||
# Сбросим все состояния (отменим создание поста, если оно было)
|
||||
#await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Правила❗️", url='https://teletype.in/@velli_arsaan/XxUiHcB4Puj'))
|
||||
ikb.row(InlineKeyboardButton(text="Назад↪️", callback_data='start'))
|
||||
|
||||
# Создание базовых переменных сообщения
|
||||
caption: str = f"""
|
||||
Приветствуем! Это бот для связи по вопросам союзов проекта ˚₊· ➳ 𝑆𝑦𝑠𝑡𝑒𝑚 𝑅𝑒𝑠𝑒𝑡 ·₊˚.
|
||||
Задайте свой вопрос, и мы постараемся ответить вам в ближайшее время — в некотором случае можем попроосить вас дать юз/ссылку на ваш проект.
|
||||
|
||||
Предложение о заключении союзов должно выглядеть вот так:
|
||||
– Название
|
||||
– Юз и ссылка на инфо
|
||||
– Юз и ссылка на лайф
|
||||
– Условия союзов
|
||||
– Юзер следящего с вашей стороны
|
||||
– Желаемый следящий с нашей стороны (мы будем в праве поставить вам другого, но тот, которого вы назовёте, будет в приоритете)
|
||||
– Кодовое предложение из условий союзов. Оно состоит из 4 слов, которые расположены в верном порядке в статье о наших условиях сотрудничества.
|
||||
|
||||
Имейте ввиду, что мы можем отказаться от союза без объяснения причин!
|
||||
"""
|
||||
|
||||
# Обработчик ответа на сообщение
|
||||
await msg(message=message, text=caption, markup=ikb)
|
||||
|
||||
# Установим состояние ожидания анкеты
|
||||
await state.set_state(UnionStates.waiting_for_union)
|
||||
12
bot/handlers/custom/__init__.py
Normal file
12
bot/handlers/custom/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# bot/handlers/__init__.py
|
||||
from aiogram import Router
|
||||
from .econom import router as economy_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ('router',)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подготовка мастер-роутера
|
||||
router.include_routers(
|
||||
economy_router,
|
||||
)
|
||||
286
bot/handlers/custom/econom.py
Normal file
286
bot/handlers/custom/econom.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# modules/economy.py
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Tuple, List
|
||||
|
||||
import aiofiles
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.types import Message, User
|
||||
from aiogram.utils.markdown import hbold
|
||||
|
||||
from bot.filters import IsOwner
|
||||
|
||||
# ==================== Конфигурация ====================
|
||||
ECONOMY_FILE = Path("data/economy.json")
|
||||
ECONOMY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
CURRENCY_NAME = "коинов"
|
||||
|
||||
|
||||
# ==================== Хранилище ====================
|
||||
class Economy:
|
||||
def __init__(self):
|
||||
self.data: Dict[int, dict] = {} # user_id → {balance, username, full_name}
|
||||
self.username_to_id: Dict[str, int] = {} # username.lower() → user_id
|
||||
|
||||
async def load(self):
|
||||
if not ECONOMY_FILE.exists():
|
||||
return
|
||||
try:
|
||||
async with aiofiles.open(ECONOMY_FILE, "r", encoding="utf-8") as f:
|
||||
content = await f.read()
|
||||
if not content.strip():
|
||||
return
|
||||
raw = json.loads(content)
|
||||
self.data = {int(uid): info for uid, info in raw.items()}
|
||||
self.username_to_id = {}
|
||||
for uid, info in self.data.items():
|
||||
username = info.get("username")
|
||||
if username:
|
||||
self.username_to_id[username.lower()] = uid
|
||||
except Exception as e:
|
||||
print(f"[Economy] Load error: {e}")
|
||||
|
||||
async def save(self):
|
||||
try:
|
||||
async with aiofiles.open(ECONOMY_FILE, "w", encoding="utf-8") as f:
|
||||
await f.write(json.dumps(self.data, indent=2, ensure_ascii=False))
|
||||
except Exception as e:
|
||||
print(f"[Economy] Save error: {e}")
|
||||
|
||||
async def ensure_user(self, user_id: int, username: Optional[str] = None, full_name: Optional[str] = None):
|
||||
"""Создаёт пользователя с 0 балансом, если его нет"""
|
||||
if user_id not in self.data:
|
||||
self.data[user_id] = {
|
||||
"balance": 50,
|
||||
"username": username,
|
||||
"full_name": full_name or "Unknown User"
|
||||
}
|
||||
if username:
|
||||
self.username_to_id[username.lower()] = user_id
|
||||
await self.save()
|
||||
|
||||
# Обновляем данные, если изменились
|
||||
updated = False
|
||||
if username and self.data[user_id]["username"] != username:
|
||||
old = self.data[user_id]["username"]
|
||||
if old and old.lower() in self.username_to_id:
|
||||
del self.username_to_id[old.lower()]
|
||||
self.data[user_id]["username"] = username
|
||||
self.username_to_id[username.lower()] = user_id
|
||||
updated = True
|
||||
|
||||
if full_name and self.data[user_id]["full_name"] != full_name:
|
||||
self.data[user_id]["full_name"] = full_name
|
||||
updated = True
|
||||
|
||||
if updated:
|
||||
await self.save()
|
||||
|
||||
async def get_balance(self, user: User) -> int:
|
||||
await self.ensure_user(user.id, user.username, user.full_name)
|
||||
return self.data[user.id]["balance"]
|
||||
|
||||
async def modify_balance(self, user_id: int, delta: int, username: Optional[str] = None, full_name: Optional[str] = None) -> int:
|
||||
await self.ensure_user(user_id, username, full_name)
|
||||
self.data[user_id]["balance"] += delta
|
||||
await self.save()
|
||||
return self.data[user_id]["balance"]
|
||||
|
||||
async def set_balance(self, user_id: int, amount: int, username: Optional[str] = None, full_name: Optional[str] = None):
|
||||
await self.ensure_user(user_id, username, full_name)
|
||||
self.data[user_id]["balance"] = amount
|
||||
await self.save()
|
||||
|
||||
async def delete_user(self, user_id: int) -> bool:
|
||||
if user_id in self.data:
|
||||
username = self.data[user_id].get("username")
|
||||
if username and username.lower() in self.username_to_id:
|
||||
del self.username_to_id[username.lower()]
|
||||
del self.data[user_id]
|
||||
await self.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def resolve_id(self, username: str) -> Optional[int]:
|
||||
return self.username_to_id.get(username.removeprefix("@").lower())
|
||||
|
||||
def get_top(self, limit: int = 20) -> List[Tuple[int, int, str, str]]:
|
||||
"""Топ только с положительным балансом"""
|
||||
items = []
|
||||
for uid, info in self.data.items():
|
||||
bal = info["balance"]
|
||||
if bal <= -1000:
|
||||
continue # ← НЕ ПОКАЗЫВАЕМ НУЛЕВЫЕ БАЛАНСЫ
|
||||
items.append((
|
||||
uid,
|
||||
bal,
|
||||
info.get("username") or "",
|
||||
info.get("full_name") or f"User#{uid}"
|
||||
))
|
||||
return sorted(items, key=lambda x: x[1], reverse=True)[:limit]
|
||||
|
||||
|
||||
economy = Economy()
|
||||
router = Router(name="economy")
|
||||
|
||||
|
||||
# ==================== Утилиты ====================
|
||||
def fmt(num: int) -> str:
|
||||
return f"{num:,}".replace(",", " ")
|
||||
|
||||
|
||||
def user_mention(user: Optional[User] = None, username: str = "", full_name: str = "") -> str:
|
||||
if user:
|
||||
if user.username:
|
||||
return f"@{user.username}"
|
||||
return hbold(user.full_name or "Unknown")
|
||||
if username:
|
||||
return f"@{username}"
|
||||
return hbold(full_name or "Unknown User")
|
||||
|
||||
|
||||
# ==================== Функция для регистрации при любом сообщении ====================
|
||||
async def register_user_on_message(message: Message):
|
||||
"""Вызывай эту функцию в глобальном обработчике сообщений"""
|
||||
if message.from_user:
|
||||
await economy.ensure_user(
|
||||
user_id=message.from_user.id,
|
||||
username=message.from_user.username,
|
||||
full_name=message.from_user.full_name
|
||||
)
|
||||
|
||||
|
||||
# ==================== Команды ====================
|
||||
|
||||
@router.message(Command("balance"))
|
||||
async def cmd_balance(message: Message, command: CommandObject):
|
||||
target = message.from_user
|
||||
|
||||
if message.reply_to_message and message.reply_to_message.from_user:
|
||||
target = message.reply_to_message.from_user
|
||||
elif command.args:
|
||||
uid = economy.resolve_id(command.args.strip())
|
||||
if uid:
|
||||
info = economy.data[uid]
|
||||
name = user_mention(username=info.get("username"), full_name=info.get("full_name"))
|
||||
await message.answer(f"Баланс {name}: {hbold(fmt(info['balance']))} {CURRENCY_NAME}")
|
||||
return
|
||||
|
||||
bal = await economy.get_balance(target)
|
||||
await message.answer(f"Баланс {user_mention(target)}: {hbold(fmt(bal))} {CURRENCY_NAME}")
|
||||
|
||||
|
||||
async def _get_target(message: Message, arg: Optional[str] = None):
|
||||
if message.reply_to_message and message.reply_to_message.from_user:
|
||||
return message.reply_to_message.from_user, None, None
|
||||
|
||||
if arg:
|
||||
username_raw = arg.strip().removeprefix("@")
|
||||
uid = economy.resolve_id("@" + username_raw) or economy.resolve_id(username_raw)
|
||||
return None, uid, username_raw
|
||||
|
||||
return message.from_user, None, None
|
||||
|
||||
|
||||
@router.message(Command("setbalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_setbalance(message: Message, command: CommandObject):
|
||||
if not command.args:
|
||||
return await message.answer("Использование: /setbalance <сумма> [@username | реплай]")
|
||||
|
||||
parts = command.args.strip().split(maxsplit=2)
|
||||
try:
|
||||
amount = int(parts[0])
|
||||
except ValueError:
|
||||
return await message.answer("Сумма должна быть числом.")
|
||||
|
||||
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
|
||||
if not target_id:
|
||||
return await message.answer("Пользователь не найден.")
|
||||
|
||||
await economy.set_balance(target_id, amount, username, user_obj.full_name if user_obj else None)
|
||||
await message.answer(f"Баланс {user_mention(user_obj, username)} → {hbold(fmt(amount))} {CURRENCY_NAME}")
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("plusbalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_plusbalance(message: Message, command: CommandObject):
|
||||
if not command.args:
|
||||
return await message.answer("Использование: /plusbalance <сумма> [@username | реплай]")
|
||||
|
||||
parts = command.args.strip().split(maxsplit=2)
|
||||
try:
|
||||
delta = int(parts[0])
|
||||
except ValueError:
|
||||
return await message.answer("Сумма должна быть числом.")
|
||||
|
||||
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
|
||||
if not target_id:
|
||||
return await message.answer("Пользователь не найден.")
|
||||
|
||||
new_bal = await economy.modify_balance(target_id, delta, username, user_obj.full_name if user_obj else None)
|
||||
await message.answer(f"{user_mention(user_obj, username)} +{fmt(delta)} → {hbold(fmt(new_bal))} {CURRENCY_NAME}")
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("minbalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_minbalance(message: Message, command: CommandObject):
|
||||
if not command.args:
|
||||
return await message.answer("Использование: /minbalance <сумма> [@username | реплай]")
|
||||
|
||||
parts = command.args.strip().split(maxsplit=2)
|
||||
try:
|
||||
delta = int(parts[0])
|
||||
except ValueError:
|
||||
return await message.answer("Сумма должна быть числом.")
|
||||
|
||||
user_obj, uid, username = await _get_target(message, parts[1] if len(parts) > 1 else None)
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
|
||||
if not target_id:
|
||||
return await message.answer("Пользователь не найден.")
|
||||
|
||||
new_bal = await economy.modify_balance(target_id, -delta, username, user_obj.full_name if user_obj else None)
|
||||
await message.answer(f"{user_mention(user_obj, username)} -{fmt(delta)} → {hbold(fmt(new_bal))} {CURRENCY_NAME}")
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("top"))
|
||||
async def cmd_top(message: Message):
|
||||
top = economy.get_top(20)
|
||||
if not top:
|
||||
return await message.answer("Топ пустой — никто ещё не заработал коины!")
|
||||
|
||||
lines = ["Топ-20 богачей:"]
|
||||
for i, (_, bal, username, full_name) in enumerate(top, 1):
|
||||
medal = ["1st", "2nd", "3rd"][i-1] if i <= 3 else f"{i}."
|
||||
name = f"@{username}" if username else hbold(full_name)
|
||||
lines.append(f"{medal} {name} — {hbold(fmt(bal))} {CURRENCY_NAME}")
|
||||
|
||||
await message.answer("\n".join(lines))
|
||||
return None
|
||||
|
||||
|
||||
@router.message(Command("deletebalance"), IsOwner(send_error_message=True))
|
||||
async def cmd_deletebalance(message: Message):
|
||||
user_obj, uid, username = await _get_target(message)
|
||||
if not (user_obj or uid):
|
||||
return await message.answer("Укажи пользователя реплаем или @username")
|
||||
|
||||
target_id = user_obj.id if user_obj else uid
|
||||
deleted = await economy.delete_user(target_id)
|
||||
name = user_mention(user_obj, username)
|
||||
await message.answer(f"Запись {name} {'удалена' if deleted else 'не существовала'}")
|
||||
return None
|
||||
|
||||
|
||||
# ==================== Запуск ====================
|
||||
async def on_startup(_):
|
||||
await economy.load()
|
||||
|
||||
|
||||
__all__ = ["router", "on_startup", "register_user_on_message"]
|
||||
16
bot/handlers/form_utils/__init__.py
Normal file
16
bot/handlers/form_utils/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# bot/handlers/__init__.py
|
||||
from aiogram import Router
|
||||
from .form_answer import router as form_answer_router
|
||||
from .topic_replies import router as topic_replies_router
|
||||
from .form_callback import router as form_callback_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ('router',)
|
||||
router: Router = Router(name="handlers_router")
|
||||
|
||||
# Подготовка мастер-роутера
|
||||
router.include_routers(
|
||||
form_answer_router,
|
||||
topic_replies_router,
|
||||
form_callback_router,
|
||||
)
|
||||
45
bot/handlers/form_utils/form_answer.py
Normal file
45
bot/handlers/form_utils/form_answer.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from bot.data.topic_map import user_topic_map
|
||||
from bot.keyboards import decision_keyboard
|
||||
from bot.states.anketa_states import StartForm
|
||||
from configs import ImportantID
|
||||
|
||||
router = Router(name="form_handlers")
|
||||
|
||||
TOPIC_TYPE = "anketa"
|
||||
|
||||
@router.message(StartForm.waiting_for_application)
|
||||
async def handle_application(message: Message, state: FSMContext):
|
||||
await state.clear()
|
||||
await message.answer("Спасибо! Ваша анкета отправлена на рассмотрение!")
|
||||
|
||||
user = message.from_user
|
||||
user_id = user.id
|
||||
if user.username:
|
||||
users = f' от @{user.username}'
|
||||
else:
|
||||
users = ''
|
||||
text = f'<b><a href="tg://user?id={user_id}">Анкета{users}</a></b>\n\n{message.html_text}'
|
||||
|
||||
key = (user_id, TOPIC_TYPE) # Ключ с типом топика
|
||||
|
||||
if key in user_topic_map:
|
||||
thread_id = user_topic_map[key]
|
||||
else:
|
||||
topic = await message.bot.create_forum_topic(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
name=f"Анкета от {user.full_name}"
|
||||
)
|
||||
thread_id = topic.message_thread_id
|
||||
user_topic_map[key] = thread_id
|
||||
|
||||
await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=text,
|
||||
reply_markup=decision_keyboard(thread_id, TOPIC_TYPE) # <--- Передаём оба аргумента
|
||||
)
|
||||
|
||||
|
||||
44
bot/handlers/form_utils/form_callback.py
Normal file
44
bot/handlers/form_utils/form_callback.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery
|
||||
from bot.data.topic_map import user_topic_map
|
||||
|
||||
router = Router(name="form_callbacks")
|
||||
|
||||
TEXTS = {
|
||||
"anketa": {
|
||||
"accept": "<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!",
|
||||
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
|
||||
},
|
||||
"application": {
|
||||
"accept": "<b>🎉 Ваша анкета принята!</b>\n\nДобро пожаловать в проект!",
|
||||
"reject": "<b>❌ Ваша анкета отклонена.</b>\n\nВы можете попробовать позже."
|
||||
},
|
||||
"union": {
|
||||
"accept": "<b>🤝 Союз одобрен!</b>\n\nТеперь вы в союзе.",
|
||||
"reject": "<b>💔 Союз отклонён.</b>\n\nВы можете обсудить детали с администрацией."
|
||||
}
|
||||
}
|
||||
|
||||
@router.callback_query(F.data.regexp(r"^([a-z_]+):(accept|reject):(\d+)$"))
|
||||
async def process_decision_callback(callback: CallbackQuery):
|
||||
kind, action, thread_id_str = callback.data.split(":")
|
||||
thread_id = int(thread_id_str)
|
||||
|
||||
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 = TEXTS.get(kind, {}).get(action)
|
||||
if not text_to_send:
|
||||
await callback.answer("Некорректные данные.", show_alert=True)
|
||||
return
|
||||
|
||||
await callback.message.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("Ответ отправлен пользователю.")
|
||||
29
bot/handlers/form_utils/topic_replies.py
Normal file
29
bot/handlers/form_utils/topic_replies.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message
|
||||
from bot.data.topic_map import user_topic_map
|
||||
|
||||
router: Router = Router(name="topic_replies")
|
||||
|
||||
@router.message(F.is_topic_message, F.reply_to_message, ~F.from_user.is_bot)
|
||||
async def forward_reply_to_user(message: Message):
|
||||
thread_id = message.message_thread_id
|
||||
if thread_id is None:
|
||||
return # нет thread_id, выходим
|
||||
|
||||
# Найдем user_id по thread_id
|
||||
user_id = None
|
||||
for (uid, kind), tid in user_topic_map.items():
|
||||
if tid == thread_id:
|
||||
user_id = uid
|
||||
break
|
||||
|
||||
if user_id is None:
|
||||
return # Топик не зарегистрирован
|
||||
|
||||
reply_text = 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}")
|
||||
|
||||
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_msg import router as default_message_router
|
||||
from .ping_test import router as ping_test_message_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подготовка роутера команд
|
||||
router.include_routers(
|
||||
ping_test_message_router,
|
||||
)
|
||||
|
||||
# Подключение стандартного роутера
|
||||
router.include_router(default_message_router)
|
||||
14
bot/handlers/messages/default.py
Normal file
14
bot/handlers/messages/default.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.handlers.custom.econom import register_user_on_message
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
@router.message()
|
||||
async def default_messages(message: Message) -> None:
|
||||
"""Обработчик всех необработанных сообщений."""
|
||||
await register_user_on_message(message)
|
||||
11
bot/handlers/messages/default_msg.py
Normal file
11
bot/handlers/messages/default_msg.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
@router.message()
|
||||
async def default_msg(message: Message) -> None:
|
||||
"""Обработчик всех необработанных сообщений."""
|
||||
return
|
||||
32
bot/handlers/messages/ping_test.py
Normal file
32
bot/handlers/messages/ping_test.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message
|
||||
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Словарь с ответами по ключам
|
||||
RESPONSE_DICT: dict[str, str] = {
|
||||
"пинг": "Понг! 🏓",
|
||||
"понг": "Пинг!",
|
||||
"бот": "На месте! 🤖",
|
||||
}
|
||||
|
||||
|
||||
@router.message()
|
||||
async def auto_response_handler(message: Message) -> None:
|
||||
"""Обработчик автоматических ответов по ключевым словам."""
|
||||
if not message.text:
|
||||
return
|
||||
|
||||
text_lower: str = message.text.casefold().strip()
|
||||
|
||||
# Поиск точного совпадения
|
||||
if text_lower in RESPONSE_DICT:
|
||||
response: str = RESPONSE_DICT[text_lower]
|
||||
await message.answer(response)
|
||||
return
|
||||
|
||||
# Поиск частичного совпадения (если хотите расширенную функциональность)
|
||||
for key, response in RESPONSE_DICT.items():
|
||||
if key in text_lower and len(key) > 3: # Только для ключей длиннее 3 символов
|
||||
await message.answer(response)
|
||||
return
|
||||
11
bot/handlers/union_utills/__init__.py
Normal file
11
bot/handlers/union_utills/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# bot/handlers/__init__.py
|
||||
from aiogram import Router
|
||||
from .union_handlers import router as union_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подготовка мастер-роутера
|
||||
router.include_routers(
|
||||
union_router,
|
||||
)
|
||||
44
bot/handlers/union_utills/union_handlers.py
Normal file
44
bot/handlers/union_utills/union_handlers.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from aiogram import Router
|
||||
from aiogram.types import Message
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from bot.data.topic_map import user_topic_map
|
||||
from bot.keyboards import decision_keyboard
|
||||
from bot.states.union_states import UnionStates
|
||||
from bot.utils import status_clear
|
||||
from configs import ImportantID
|
||||
|
||||
router: Router = Router(name="union_handlers")
|
||||
|
||||
|
||||
|
||||
@router.message(UnionStates.waiting_for_union)
|
||||
async def handle_union(message: Message, state: FSMContext) -> None:
|
||||
await message.answer("Спасибо! Ваше сообщение отправлено.")
|
||||
|
||||
user = message.from_user
|
||||
user_id = user.id
|
||||
msg_type = "union"
|
||||
text = f"<b>СОЮЗ</b>\n\n{message.html_text}"
|
||||
|
||||
key = (user_id, msg_type)
|
||||
|
||||
if key in user_topic_map:
|
||||
thread_id = user_topic_map[key]
|
||||
else:
|
||||
topic = await message.bot.create_forum_topic(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
name=f"Сообщение от {user.full_name}"
|
||||
)
|
||||
thread_id = topic.message_thread_id
|
||||
user_topic_map[key] = thread_id
|
||||
|
||||
await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=724,
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=decision_keyboard(thread_id, "union")
|
||||
)
|
||||
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
2
bot/keyboards/__init__.py
Normal file
2
bot/keyboards/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .inline import *
|
||||
from .reply import *
|
||||
1
bot/keyboards/inline/__init__.py
Normal file
1
bot/keyboards/inline/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .decision import *
|
||||
18
bot/keyboards/inline/decision.py
Normal file
18
bot/keyboards/inline/decision.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
|
||||
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
55
bot/middlewares/__init__.py
Normal file
55
bot/middlewares/__init__.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from aiogram import Dispatcher, Bot
|
||||
|
||||
from configs import ImportantID
|
||||
from .error_mdw import ErrorHandlingMiddleware
|
||||
from .logging_mdw import LoggingMiddleware
|
||||
from .msg_mdw import MessageCounterMiddleware
|
||||
from .referal_mdw import ReferralMiddleware
|
||||
from .spam_mdw import RateLimitMiddleware
|
||||
from .subscription_mdw import SubscriptionMiddleware
|
||||
from .time_mdw import TimingMiddleware
|
||||
from .ban_user_mdw import BanCheckMiddleware
|
||||
|
||||
# Настройки экспорта
|
||||
__all__ = (
|
||||
"LoggingMiddleware",
|
||||
"SubscriptionMiddleware",
|
||||
"RateLimitMiddleware",
|
||||
"ErrorHandlingMiddleware",
|
||||
"TimingMiddleware",
|
||||
"MessageCounterMiddleware",
|
||||
"setup_middlewares",
|
||||
"ReferralMiddleware",
|
||||
"BanCheckMiddleware",
|
||||
)
|
||||
|
||||
|
||||
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 = [
|
||||
BanCheckMiddleware(),
|
||||
# RateLimitMiddleware(rate_limit=3, time_period=5.0), # Антифлуд
|
||||
# SubscriptionMiddleware(bot=bot, channel_ids=channel_ids), # Проверка подписки
|
||||
MessageCounterMiddleware(), # Подсчет сообщений
|
||||
ReferralMiddleware(), # Проверка реф-ссылок
|
||||
]
|
||||
|
||||
# Регистрируем middleware для всех событий
|
||||
for middleware in middlewares_updates:
|
||||
dp.update.middleware(middleware)
|
||||
|
||||
# Регистрируем middleware только для сообщений
|
||||
for middleware in middlewares_msg:
|
||||
dp.message.middleware(middleware)
|
||||
108
bot/middlewares/ban_user_mdw.py
Normal file
108
bot/middlewares/ban_user_mdw.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import Message, CallbackQuery, TelegramObject
|
||||
|
||||
from database import db
|
||||
|
||||
__all__ = ("BanCheckMiddleware",)
|
||||
|
||||
class BanCheckMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для проверки забанен ли пользователь.
|
||||
Если пользователь забанен в боте - блокирует все его действия.
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Проверяет каждый входящий запрос на наличие пользователя в черном списке.
|
||||
|
||||
Args:
|
||||
handler: Следующий обработчик
|
||||
event: Событие (сообщение, callback и т.д.)
|
||||
data: Данные контекста
|
||||
|
||||
Returns:
|
||||
Результат обработчика или None если пользователь забанен
|
||||
"""
|
||||
# Извлекаем информацию о пользователе из события
|
||||
user = await self._extract_user(event)
|
||||
|
||||
if user is None:
|
||||
# Не смогли определить пользователя - пропускаем
|
||||
return await handler(event, data)
|
||||
|
||||
# Проверяем в базе данных статус пользователя
|
||||
user_db = await db.get_user(user.id)
|
||||
|
||||
if user_db and user_db.status == "banned":
|
||||
# Пользователь забанен - блокируем запрос
|
||||
await self._send_ban_message(event, data)
|
||||
return None
|
||||
|
||||
# Пользователь не забанен - пропускаем запрос дальше
|
||||
return await handler(event, data)
|
||||
|
||||
@staticmethod
|
||||
async def _extract_user(event: TelegramObject) -> Any:
|
||||
"""
|
||||
Извлекает пользователя из разных типов событий.
|
||||
|
||||
Args:
|
||||
event: Событие Telegram
|
||||
|
||||
Returns:
|
||||
Объект пользователя или None
|
||||
"""
|
||||
if isinstance(event, Message):
|
||||
return event.from_user
|
||||
elif isinstance(event, CallbackQuery):
|
||||
return event.from_user
|
||||
# Можно добавить другие типы событий при необходимости
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _send_ban_message(event: TelegramObject, data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Отправляет сообщение о бане пользователю.
|
||||
|
||||
Args:
|
||||
event: Событие которое triggered проверку
|
||||
data: Данные контекста с ботом
|
||||
"""
|
||||
bot = data.get('bot')
|
||||
|
||||
if not bot:
|
||||
return
|
||||
|
||||
chat_id = None
|
||||
message_id = None
|
||||
|
||||
# Определяем куда отправлять сообщение в зависимости от типа события
|
||||
if isinstance(event, Message):
|
||||
chat_id = event.chat.id
|
||||
message_id = event.message_id
|
||||
elif isinstance(event, CallbackQuery) and event.message:
|
||||
chat_id = event.message.chat.id
|
||||
message_id = event.message.message_id
|
||||
|
||||
if chat_id:
|
||||
try:
|
||||
if isinstance(event, CallbackQuery):
|
||||
# Для callback запросов отвечаем уведомлением
|
||||
await event.answer("🚫 Вы заблокированы в боте!", show_alert=True)
|
||||
else:
|
||||
# Для сообщений отправляем новое сообщение
|
||||
await bot.send_message(
|
||||
chat_id=chat_id,
|
||||
text="🚫 Вы заблокированы в боте!",
|
||||
reply_to_message_id=message_id
|
||||
)
|
||||
except Exception:
|
||||
# Игнорируем ошибки отправки (пользователь мог заблокировать бота)
|
||||
pass
|
||||
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 Bot, BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
|
||||
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: str = self._extract_user_info(event)
|
||||
|
||||
# Логируем ошибку
|
||||
error_message: str = f"Ошибка в хендлере: {type(e).__name__}: {str(e)}"
|
||||
|
||||
logger.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: 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: str = ""
|
||||
|
||||
# Для Message
|
||||
if isinstance(event, Message) and hasattr(event, 'text') and event.text:
|
||||
event_text: str = event.text
|
||||
# Для CallbackQuery
|
||||
elif isinstance(event, CallbackQuery) and hasattr(event, 'data') and event.data:
|
||||
event_text: str = f"callback: {event.data}"
|
||||
# Для Update
|
||||
elif isinstance(event, Update):
|
||||
if event.message and event.message.text:
|
||||
event_text: str = event.message.text
|
||||
elif event.callback_query and event.callback_query.data:
|
||||
event_text: str = f"callback: {event.callback_query.data}"
|
||||
elif event.edited_message and event.edited_message.text:
|
||||
event_text: str = 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:
|
||||
"""Уведомляет администраторов об ошибке."""
|
||||
bot: Bot = event.bot if hasattr(event, 'bot') else None
|
||||
|
||||
if bot:
|
||||
for admin_id in self.admin_ids:
|
||||
try:
|
||||
event_info: str = f"Событие: {type(event).__name__}"
|
||||
event_text: str = self._extract_event_text(event)
|
||||
if event_text:
|
||||
event_info += f", текст: {event_text}"
|
||||
|
||||
full_message: str = (
|
||||
f"🚨 Ошибка в боте:\n\n"
|
||||
f"Пользователь: {user_str}\n"
|
||||
f"Ошибка: {error_message}\n"
|
||||
f"{event_info}"
|
||||
)
|
||||
|
||||
await bot.send_message(admin_id, full_message)
|
||||
|
||||
logger.info(
|
||||
text=f"Администратор {admin_id} уведомлен об ошибке",
|
||||
log_type="ADMIN_NOTIFIED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.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: str = (
|
||||
"⚠️ Произошла непредвиденная ошибка. "
|
||||
"Разработчики уже уведомлены и работают над исправлением.\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)
|
||||
|
||||
logger.info(
|
||||
text="Пользователю отправлено сообщение об ошибке",
|
||||
log_type="ERROR_MESSAGE_SENT",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
text=f"Не удалось отправить сообщение об ошибке: {e}",
|
||||
log_type="ERROR_MESSAGE_FAILED",
|
||||
user=user_str
|
||||
)
|
||||
272
bot/middlewares/logging_mdw.py
Normal file
272
bot/middlewares/logging_mdw.py
Normal file
@@ -0,0 +1,272 @@
|
||||
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 configs import BotSettings, COMMANDS # импортируем настройки и команды
|
||||
from middleware.loggers import logger # ваш глобальный логгер
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# Логируем получение события с префиксом проекта
|
||||
logger.info(
|
||||
text=log_text,
|
||||
log_type=prefixed_log_type,
|
||||
user=user_str
|
||||
)
|
||||
|
||||
try:
|
||||
# Передаем событие следующему обработчику
|
||||
result: Any = await handler(event, data)
|
||||
|
||||
# Логируем успешное выполнение для команд
|
||||
if log_type == "CMD":
|
||||
logger.info(
|
||||
text=f"[SUCCESS] команда обработана",
|
||||
log_type=prefixed_log_type,
|
||||
user=user_str
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Логируем ошибку при обработке с префиксом проекта
|
||||
logger.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: Message | None = (
|
||||
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: Message | None = 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: User | None = 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: User | None = 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: User | None = 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: Message | None = 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
|
||||
57
bot/middlewares/msg_mdw.py
Normal file
57
bot/middlewares/msg_mdw.py
Normal file
@@ -0,0 +1,57 @@
|
||||
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: int = message.from_user.id
|
||||
message_text: str = 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,
|
||||
)
|
||||
59
bot/middlewares/referal_mdw.py
Normal file
59
bot/middlewares/referal_mdw.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from typing import Callable, Awaitable, Any, Dict, Optional
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.filters.command import CommandObject
|
||||
from aiogram.types import TelegramObject, Message
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
|
||||
class ReferralMiddleware(BaseMiddleware):
|
||||
"""
|
||||
Middleware для перехвата и обработки реферальных ссылок (?start=...).
|
||||
|
||||
Основные задачи:
|
||||
- Отслеживание перехода по deep-link (например, /start ref123)
|
||||
- Централизованное логирование
|
||||
- Возможность передачи кода дальше в хендлеры
|
||||
- Подготовка к сохранению кода в базу данных
|
||||
"""
|
||||
|
||||
async def __call__(
|
||||
self,
|
||||
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
||||
event: TelegramObject,
|
||||
data: Dict[str, Any],
|
||||
) -> Any:
|
||||
"""
|
||||
Перехватывает входящие сообщения и извлекает deep-link аргумент,
|
||||
если пользователь зашёл по реферальной ссылке.
|
||||
|
||||
Args:
|
||||
handler: Следующий обработчик в цепочке middleware
|
||||
event: Входящее событие (Message, CallbackQuery и др.)
|
||||
data: Контекстные данные, доступные хендлеру
|
||||
|
||||
Returns:
|
||||
Результат работы следующего обработчика
|
||||
"""
|
||||
# Проверяем, что событие — это именно сообщение
|
||||
if isinstance(event, Message):
|
||||
# Извлекаем объект команды (если был установлен фильтр CommandStart)
|
||||
command: Optional[CommandObject] = data.get("command")
|
||||
|
||||
# Проверяем, что это именно команда /start с аргументом
|
||||
if command and command.command.casefold() == "start" and command.args:
|
||||
ref_code: str = command.args
|
||||
user_id: int = event.from_user.id
|
||||
username: Optional[str] = event.from_user.username
|
||||
|
||||
# 👉 Здесь можно сохранить код в БД
|
||||
logger.debug(
|
||||
f"[Referral] user={user_id}, username={username}, ref={ref_code}"
|
||||
)
|
||||
|
||||
# Пробрасываем реф-код в data, чтобы использовать в хендлере
|
||||
data["ref_code"] = ref_code
|
||||
|
||||
# Передаём управление дальше
|
||||
return await handler(event, data)
|
||||
98
bot/middlewares/spam_mdw.py
Normal file
98
bot/middlewares/spam_mdw.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery
|
||||
|
||||
from middleware.loggers import logger # ваш логгер
|
||||
|
||||
|
||||
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]: dict[int, list[float]] = [
|
||||
call_time for call_time in self.user_calls[user_id]
|
||||
if current_time - call_time < self.time_period
|
||||
]
|
||||
|
||||
# Логируем текущее состояние rate limit
|
||||
if log:
|
||||
logger.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:
|
||||
logger.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)
|
||||
|
||||
logger.debug(
|
||||
text=f"Запрос добавлен в rate limit",
|
||||
log_type="RATE_LIMIT_ADDED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
return await handler(event, data)
|
||||
110
bot/middlewares/subscription_mdw.py
Normal file
110
bot/middlewares/subscription_mdw.py
Normal file
@@ -0,0 +1,110 @@
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware, Bot
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
|
||||
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}"
|
||||
|
||||
# Логируем начало проверки подписки
|
||||
logger.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:
|
||||
logger.error(
|
||||
text=f"Ошибка проверки подписки на канал {channel_id}: {e}",
|
||||
log_type="SUBSCRIPTION_ERROR",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Если пользователь не подписан на некоторые каналы
|
||||
if not_subscribed_channels:
|
||||
logger.warning(
|
||||
text=f"Пользователь не подписан на каналы: {', '.join(not_subscribed_channels)}",
|
||||
log_type="SUBSCRIPTION_FAILED",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
warning_text: str = (
|
||||
"📢 Для использования бота необходимо подписаться на наши каналы!\n\n"
|
||||
"После подписки нажмите /start для продолжения."
|
||||
)
|
||||
|
||||
# Создаем кнопку "Проверить подписку"
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
|
||||
|
||||
if isinstance(event, Message):
|
||||
await event.answer(warning_text, reply_markup=ikb.as_markup())
|
||||
elif isinstance(event, CallbackQuery):
|
||||
await event.message.answer(warning_text, reply_markup=ikb.as_markup())
|
||||
await event.answer()
|
||||
|
||||
return None
|
||||
|
||||
# Логируем успешную проверку подписки
|
||||
logger.info(
|
||||
text="Пользователь подписан на все required каналы",
|
||||
log_type="SUBSCRIPTION_SUCCESS",
|
||||
user=user_str
|
||||
)
|
||||
|
||||
# Если подписка есть, продолжаем обработку
|
||||
return await handler(event, data)
|
||||
83
bot/middlewares/time_mdw.py
Normal file
83
bot/middlewares/time_mdw.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from time import time
|
||||
from typing import Callable, Awaitable, Any, Dict
|
||||
|
||||
from aiogram import BaseMiddleware
|
||||
from aiogram.types import TelegramObject, Message, CallbackQuery, Update
|
||||
|
||||
from middleware.loggers import logger
|
||||
|
||||
|
||||
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: Any = 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: # Медленные запросы
|
||||
logger.warning(
|
||||
text=f"Медленный хендлер: {execution_time:.2f}сек",
|
||||
log_type="SLOW_HANDLER",
|
||||
user=user_str
|
||||
)
|
||||
elif execution_time > 0.5 and perm == "medium": # Средние запросы
|
||||
logger.info(
|
||||
text=f"Среднее время выполнения: {execution_time:.3f}сек",
|
||||
log_type="HANDLER_TIMING",
|
||||
user=user_str
|
||||
)
|
||||
elif perm == "fast": # Быстрые запросы
|
||||
logger.debug(
|
||||
text=f"Быстрое выполнение: {execution_time:.3f}сек",
|
||||
log_type="HANDLER_TIMING_FAST",
|
||||
user=user_str
|
||||
)
|
||||
2
bot/states/__init__.py
Normal file
2
bot/states/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .anketa_states import *
|
||||
from .new_states import *
|
||||
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()
|
||||
10
bot/states/new_states.py
Normal file
10
bot/states/new_states.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# bot/states/new_states.py
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
__all__ = ("NewStates",)
|
||||
|
||||
class NewStates(StatesGroup):
|
||||
role: State = State()
|
||||
sorol: State = State()
|
||||
code_phrase: State = State()
|
||||
rules: State = State()
|
||||
5
bot/states/union_states.py
Normal file
5
bot/states/union_states.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# bot/states/union_states.py
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
|
||||
class UnionStates(StatesGroup):
|
||||
waiting_for_union = State()
|
||||
1
bot/templates/__init__.py
Normal file
1
bot/templates/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .message_callback import *
|
||||
88
bot/templates/message_callback.py
Normal file
88
bot/templates/message_callback.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from typing import Union
|
||||
|
||||
from aiogram.types import (
|
||||
FSInputFile,
|
||||
CallbackQuery,
|
||||
Message,
|
||||
ReplyKeyboardMarkup,
|
||||
InlineKeyboardMarkup
|
||||
)
|
||||
from aiogram.utils.keyboard import ReplyKeyboardBuilder, InlineKeyboardBuilder
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ('msg', 'msg_photo', 'markups',)
|
||||
|
||||
|
||||
def markups(markup: Union[
|
||||
InlineKeyboardBuilder,
|
||||
ReplyKeyboardBuilder,
|
||||
InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
None] = None, ) -> None:
|
||||
"""Получение маркапа"""
|
||||
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)
|
||||
elif isinstance(markup, (InlineKeyboardMarkup, ReplyKeyboardMarkup)):
|
||||
reply_markup = markup
|
||||
return reply_markup
|
||||
|
||||
|
||||
async def msg(
|
||||
message: Message | CallbackQuery,
|
||||
text: str = "Сообщение отправлено!",
|
||||
markup: Union[
|
||||
InlineKeyboardBuilder,
|
||||
ReplyKeyboardBuilder,
|
||||
InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
None,
|
||||
] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Шаблон для отправки текстового сообщения или ответа на callback-запрос.
|
||||
|
||||
Args:
|
||||
message (Message | CallbackQuery): Сообщение или callback-запрос.
|
||||
text (str): Текст сообщения.
|
||||
markup (Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, InlineKeyboardMarkup, ReplyKeyboardMarkup, None]):
|
||||
Клавиатура для сообщения. Может быть билдера или готовый объект.
|
||||
"""
|
||||
|
||||
if isinstance(message, Message):
|
||||
await message.reply(text=text, reply_markup=markups(markup))
|
||||
else:
|
||||
await message.message.reply(text=text, reply_markup=markups(markup))
|
||||
|
||||
|
||||
async def msg_photo(
|
||||
message: Message | CallbackQuery,
|
||||
file: str = "assets/default.jpg",
|
||||
text: str = "Сообщение отправлено!",
|
||||
markup: Union[
|
||||
InlineKeyboardBuilder,
|
||||
ReplyKeyboardBuilder,
|
||||
InlineKeyboardMarkup,
|
||||
ReplyKeyboardMarkup,
|
||||
None,
|
||||
] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Шаблон для отправки фотографии с подписью или ответа на callback-запрос.
|
||||
|
||||
Args:
|
||||
message (Message | CallbackQuery): Сообщение или callback-запрос.
|
||||
file (str): Путь к файлу фотографии.
|
||||
text (str): Подпись к фото.
|
||||
markup (Union[InlineKeyboardBuilder, ReplyKeyboardBuilder, InlineKeyboardMarkup, ReplyKeyboardMarkup, None]):
|
||||
Клавиатура для сообщения. Может быть билдера или готовый объект.
|
||||
"""
|
||||
|
||||
if isinstance(message, Message):
|
||||
await message.reply_photo(photo=FSInputFile(file), caption=text, reply_markup=markups(markup))
|
||||
else:
|
||||
await message.message.reply_photo(photo=FSInputFile(file), caption=text, reply_markup=markups(markup))
|
||||
9
bot/utils/__init__.py
Normal file
9
bot/utils/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .argument import *
|
||||
from .clear_status import *
|
||||
from .format_time import *
|
||||
from .interesting_facts import *
|
||||
from .pagination import *
|
||||
from .type_message import *
|
||||
from .usernames import *
|
||||
from .auto_delete import *
|
||||
from .hidden_username import *
|
||||
61
bot/utils/argument.py
Normal file
61
bot/utils/argument.py
Normal file
@@ -0,0 +1,61 @@
|
||||
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
|
||||
19
bot/utils/auto_delete.py
Normal file
19
bot/utils/auto_delete.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from asyncio import sleep
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from bot import bot
|
||||
from middleware import logger
|
||||
|
||||
__all__ = ("auto_delete_message",)
|
||||
|
||||
async def auto_delete_message(chat_id: int, message_id: int, delay: int = 604800) -> None:
|
||||
"""
|
||||
Автоматически удаляет сообщение через указанный промежуток времени.
|
||||
По умолчанию — 7 суток (604800 секунд).
|
||||
"""
|
||||
await sleep(delay=delay)
|
||||
try:
|
||||
await bot.delete_message(chat_id=chat_id, message_id=message_id)
|
||||
logger.info("Закрепленное сообщение удалено")
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"[ALL] Ошибка при автоудалении: {e}")
|
||||
17
bot/utils/clear_status.py
Normal file
17
bot/utils/clear_status.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("status_clear", "inline_clear")
|
||||
|
||||
|
||||
async def inline_clear(message: Message | CallbackQuery) -> None:
|
||||
"""Очищает все статусы инлайн сообщений"""
|
||||
if isinstance(message, CallbackQuery):
|
||||
await message.answer()
|
||||
|
||||
|
||||
async def status_clear(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Очищает все статусы, и отвечает на сообщения"""
|
||||
await state.clear()
|
||||
await inline_clear(message=message)
|
||||
23
bot/utils/format_time.py
Normal file
23
bot/utils/format_time.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("format_retry_time",)
|
||||
|
||||
|
||||
def format_retry_time(retry_after: int) -> str:
|
||||
"""
|
||||
Форматирование времени повторной попытки в читаемом виде.
|
||||
|
||||
Args:
|
||||
retry_after (int): Время в секундах до следующей попытки.
|
||||
|
||||
Returns:
|
||||
str: Строка в формате X часов, Y минут, Z секунд.
|
||||
"""
|
||||
hours, remainder = divmod(retry_after, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours} часов, {minutes} минут, {seconds} секунд"
|
||||
elif minutes > 0:
|
||||
return f"{minutes} минут, {seconds} секунд"
|
||||
else:
|
||||
return f"{seconds} секунд"
|
||||
21
bot/utils/hidden_username.py
Normal file
21
bot/utils/hidden_username.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram.types import Message
|
||||
from aiogram.utils.markdown import hide_link
|
||||
|
||||
from bot import bot
|
||||
|
||||
__all__ = ("hidden_admins_message",)
|
||||
|
||||
|
||||
async def hidden_admins_message(message: Message,
|
||||
text: str = "") -> str:
|
||||
"""
|
||||
Формирует текст с упоминанием всех админов через скрытые ссылки.
|
||||
"""
|
||||
admins = await bot.get_chat_administrators(message.chat.id)
|
||||
|
||||
hidden_links: str = "".join(
|
||||
hide_link(f"tg://user?id={admin.user.id}")
|
||||
for admin in admins if not admin.user.is_bot
|
||||
)
|
||||
|
||||
return f"{hidden_links}{text}"
|
||||
29
bot/utils/interesting_facts.py
Normal file
29
bot/utils/interesting_facts.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from random import choice
|
||||
|
||||
from configs.config import Lists
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ("interesting_fact",)
|
||||
|
||||
|
||||
def interesting_fact(mode: str = "факт", lists: list[str] = None) -> str:
|
||||
"""
|
||||
Возвращает случайный факт, анекдот или цитату, в зависимости от режима.
|
||||
|
||||
:param mode: Строка, определяющая тип контента ("факт", "анекдот", "цитата").
|
||||
:param lists: Необязательный список строк, из которого можно выбирать вручную.
|
||||
:return: Случайный элемент из соответствующего списка.
|
||||
"""
|
||||
if lists is not None:
|
||||
return choice(lists)
|
||||
|
||||
mode: str = 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)
|
||||
29
bot/utils/pagination.py
Normal file
29
bot/utils/pagination.py
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
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}")
|
||||
23
bot/utils/usernames.py
Normal file
23
bot/utils/usernames.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
|
||||
# Настройка экспорта в модули
|
||||
__all__ = ('username',)
|
||||
|
||||
|
||||
# Функция получения юзера или ID пользователя
|
||||
def username(message: Message | CallbackQuery) -> 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