Первый коммит

This commit is contained in:
2026-02-17 11:24:55 +07:00
commit a06448ca4b
109 changed files with 21165 additions and 0 deletions

14
bot/handlers/__init__.py Normal file
View File

@@ -0,0 +1,14 @@
from aiogram import Router
from .commands import router as cmd_routers
from .messages import router as messages_routers
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
cmd_routers,
messages_routers,
)

View File

@@ -0,0 +1,16 @@
from aiogram import Router
#from .admins import router as admin_cmd_router
from .users import router as users_cmd_router
#from .settings import router as settings_cmd_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
#settings_cmd_router,
#admin_cmd_router,
users_cmd_router,
)

View 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,
)

View File

@@ -0,0 +1,81 @@
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),
F.chat.type.in_({"supergroup", "group"}),
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(update=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}")

View 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(update=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(update=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(update=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("Не удалось получить список забаненных пользователей")

View File

@@ -0,0 +1,277 @@
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(update=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}) кикнут из чата!",
)
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(update=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(update=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)

View File

@@ -0,0 +1,77 @@
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 для закрепления последнего сообщения или ответа.
"""
# Если есть reply → закрепляем его, иначе закрепляем предыдущее сообщение
if message.reply_to_message:
target_message_id = message.reply_to_message.message_id
else:
# Закрепляем предыдущее сообщение (команда - 1)
target_message_id = message.message_id - 1
try:
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(update=message, text="✅ Сообщение успешно закреплено", state=state)
except Exception as e:
await msg(update=message, text=f"❌ Ошибка закрепления: {e}", state=state)
@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(update=callback.message, state=state)
try:
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("✅ Сообщение закреплено")
except Exception as e:
await callback.answer(f"❌ Ошибка: {e}", show_alert=True)

View 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=CustomConfig.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(update=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)

View 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,
)

View File

@@ -0,0 +1,173 @@
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(
update=message,
text=_("❌ Короткое описание бота должно быть не более 120 символов. Текущая длина: {length}").format(
length=len(description)
),
markup=settings_keyboard(),
state=state
)
return
try:
# Установка нового короткого описания
await bot.set_my_short_description(short_description=description)
# Сохраняем текущее значение в BotInfo
BotInfo.short_description = description
# Сбрасываем состояние FSM
await state.clear()
# Отправляем сообщение об успехе
await msg(
update=message,
text=_("✅ Короткое описание бота успешно изменено на: <b>{description}</b>").format(
description=description
),
markup=settings_keyboard(),
state=state
)
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(
update=message,
text=_("⚠️ Слишком частая смена короткого описания!\nПопробуйте снова через: <b>{retry_text}</b>").format(
retry_text=retry_text
),
markup=settings_keyboard(),
state=state
)
except TelegramAPIError as e:
logger.error(f"Ошибка Telegram API при изменении короткого описания: {e}")
await msg(
update=message,
text=_("❌ Ошибка Telegram API при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
state=state
)
except Exception as e:
logger.error(f"Непредвиденная ошибка при изменении короткого описания: {e}")
await msg(
update=message,
text=_("❌ Непредвиденная ошибка при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
state=state
)
@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(
update=message,
text=_("❌ Короткое описание не должно превышать 120 символов. Текущая длина: {length}").format(
length=len(description)
),
markup=settings_keyboard(),
state=state
)
return
await handle_set_bot_description(description, message, state, bot)
return
# Вариант 2: без аргумента → включаем FSM
await status_clear(update=message, state=state)
text: str = _(
"📝 <b>Смена короткого описания бота</b>\n\n"
"Текущее короткое описание: <i>{current}</i>\n\n"
"Введите новое короткое описание (максимум 120 символов):"
).format(current=current_description)
await msg(update=message, text=text, markup=settings_keyboard(), state=state)
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)

View File

@@ -0,0 +1,157 @@
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(
update=message,
text=_("❌ Имя бота должно быть не более 64 символов. Текущая длина: {length}").format(
length=len(new_name)
),
markup=settings_keyboard(),
state=state
)
return
try:
await bot.set_my_name(new_name)
BotInfo.first_name = new_name
await state.clear()
await msg(
update=message,
text=_("✅ Имя бота успешно изменено на: <b>{new_name}</b>").format(new_name=new_name),
markup=settings_keyboard(),
state=state
)
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(
update=message,
text=_("⚠️ Слишком частая смена имени!\nПопробуйте снова через: <b>{retry_text}</b>").format(
retry_text=retry_text
),
markup=settings_keyboard(),
state=state
)
except TelegramAPIError as e:
logger.error(f"Ошибка Telegram API при изменении имени: {e}")
await msg(
update=message,
text=_("❌ Ошибка Telegram API: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
state=state
)
except Exception as e:
logger.error(f"Непредвиденная ошибка при изменении имени: {e}")
await msg(
update=message,
text=_("❌ Непредвиденная ошибка: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
state=state
)
@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(
update=message,
text=_("❌ Имя не должно превышать 64 символа. Текущая длина: {length}").format(
length=len(new_name)
),
markup=settings_keyboard(),
state=state
)
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(update=message, text=text, markup=settings_keyboard(), state=state)
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)

View File

@@ -0,0 +1,174 @@
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(
update=message,
text=_("❌ Виджет бота должен быть не более 512 символов. Текущая длина: {length}").format(
length=len(new_widget)
),
markup=settings_keyboard(),
state=state
)
return
try:
# Устанавливаем описание через Telegram API
await bot.set_my_description(description=new_widget)
# Сохраняем в BotInfo для локального использования
BotInfo.widget = new_widget
# Очищаем состояние FSM
await state.clear()
# Отправляем уведомление пользователю
await msg(
update=message,
text=_("✅ Виджет бота успешно изменён на: <b>{new_widget}</b>").format(
new_widget=new_widget
),
markup=settings_keyboard(),
state=state
)
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(
update=message,
text=_("⚠️ Слишком частая смена виджета!\nПопробуйте снова через: <b>{retry_text}</b>").format(
retry_text=retry_text
),
markup=settings_keyboard(),
state=state
)
except TelegramAPIError as e:
# Ошибка Telegram API
logger.error(f"Ошибка Telegram API при изменении виджета: {e}")
await msg(
update=message,
text=_("❌ Ошибка Telegram API при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
state=state
)
except Exception as e:
# Непредвиденная ошибка
logger.error(f"Непредвиденная ошибка при изменении виджета: {e}")
await msg(
update=message,
text=_("❌ Непредвиденная ошибка при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
markup=settings_keyboard(),
state=state
)
@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.short_description
# Вариант 1: пользователь ввёл аргумент сразу (/set_widget TEXT)
if command and command.args:
new_widget: str = command.args.strip()
if len(new_widget) > 512:
await msg(
update=message,
text=_("❌ Виджет не должен превышать 512 символов. Текущая длина: {length}").format(
length=len(new_widget)
),
markup=settings_keyboard(),
state=state
)
return
await handle_set_widget(new_widget, message, state, bot)
return
# Вариант 2: Callback query или пустая команда → запускаем FSM
await status_clear(update=message, state=state)
text: str = _(
"📝 <b>Смена виджета бота</b>\n\n"
"Текущий виджет: <i>{current}</i>\n\n"
"Пожалуйста, введите новый виджет для бота (максимум 512 символов):"
).format(current=current_widget)
await msg(update=message, text=text, markup=settings_keyboard(), state=state)
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)

View 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(update=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(update=message, text=text, markup=ikb, state=state)

View File

@@ -0,0 +1,33 @@
from aiogram import Router
from .start_cmd import router as start_cmd_router
from .listwords import router as listwords_cmd_router
from .word import router as word_cmd_router
from .slience import router as slice_router
from .conflict import router as conflict_router
from .stats import router as stats_router
from .report import router as report_router
from .admins import router as admin_router
from .notifications import router as notifications_router
from .id import router as id_router
from .emoji import router as emoji_router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
notifications_router,
report_router,
admin_router,
start_cmd_router,
listwords_cmd_router,
word_cmd_router,
slice_router,
conflict_router,
stats_router,
id_router,
emoji_router,
)

View File

@@ -0,0 +1,434 @@
"""
Обработчики команд управления администраторами
"""
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.filters.admin import IsSuperAdmin
from configs import settings, COMMANDS
from database import get_manager
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
router: Router = Router(name="admin_management_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def parse_user_id(text: str, command: str) -> tuple[bool, str | int]:
"""
Парсит ID пользователя из команды.
Args:
text: Полный текст сообщения
command: Название команды
Returns:
(success, result): result это либо user_id (int), либо текст ошибки (str)
"""
parts = text.split(maxsplit=1)
if len(parts) < 2:
return False, f"❌ Использование: <code>/{command} <ID></code>"
user_id_str = parts[1].strip()
# Валидация ID
try:
user_id = int(user_id_str)
if user_id <= 0:
return False, "❌ ID должен быть положительным числом"
if user_id > 9999999999: # Максимальный Telegram ID
return False, "❌ Некорректный ID пользователя"
return True, user_id
except ValueError:
return False, "❌ ID должен быть числом"
def format_admin_info(user_id: int, username: str | None = None) -> str:
"""Форматирует информацию об админе"""
if username:
return f"<code>{user_id}</code> (@{username})"
return f"<code>{user_id}</code>"
def get_refresh_admins_kb():
"""Клавиатура для обновления списка админов"""
ikb = InlineKeyboardBuilder()
ikb.button(text="🔄 Обновить", callback_data="listadmins:refresh")
ikb.button(text=" Добавить", callback_data="admin:help_add")
ikb.adjust(2)
return ikb.as_markup()
# ================= ДОБАВЛЕНИЕ АДМИНИСТРАТОРА =================
@router.message(Command(*COMMANDS.get("addadmin", ["addadmin"]), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="ADD_ADMIN", log_args=True)
async def add_admin_cmd(message: Message) -> None:
"""
Добавляет нового администратора бота.
Доступно только владельцам бота (OWNER_ID).
Использование: /addadmin <ID>
Пример: /addadmin 123456789
"""
success, result = parse_user_id(message.text, "addadmin")
if not success:
await message.answer(result, parse_mode="HTML")
return
user_id = result
# Проверка: нельзя добавить самого себя
if user_id == message.from_user.id:
await message.answer(
"⚠️ <b>Вы уже владелец бота</b>\n\n"
"Вам не нужно добавлять себя в администраторы",
parse_mode="HTML"
)
return
# Проверка: нельзя добавить другого владельца
if user_id in settings.OWNER_ID:
await message.answer(
"⚠️ <b>Этот пользователь уже владелец бота</b>\n\n"
"Владельцы имеют полные права автоматически",
parse_mode="HTML"
)
return
manager = get_manager()
try:
# Проверяем, уже админ ли
is_already_admin = await manager.is_admin(user_id)
if is_already_admin:
await message.answer(
f"⚠️ Пользователь {format_admin_info(user_id)} уже является администратором",
parse_mode="HTML"
)
return
# Добавляем администратора
added = await manager.add_admin(
user_id=user_id,
added_by=message.from_user.id
)
if added:
text = (
f"✅ <b>Администратор добавлен</b>\n\n"
f"👤 ID: {format_admin_info(user_id)}\n"
f"👑 Добавил: {format_admin_info(message.from_user.id, message.from_user.username)}\n\n"
f"📋 Права администратора:\n"
f"├─ Управление банвордами\n"
f"├─ Просмотр статистики\n"
f"├─ Активация режимов модерации\n"
f"└─ Все команды бота\n\n"
f"⚠️ <i>Не может управлять другими админами</i>\n"
f"Список админов: /listadmins"
)
logger.info(
f"Администратор добавлен: {user_id} (добавил: {message.from_user.id})",
log_type="ADMIN_MGMT"
)
else:
text = "❌ <b>Ошибка добавления администратора</b>\n\nПопробуйте позже"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления администратора: {e}", log_type="ADMIN_MGMT")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
# ================= УДАЛЕНИЕ АДМИНИСТРАТОРА =================
@router.message(Command(*COMMANDS.get("remadmin", ["remadmin"]), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="REMOVE_ADMIN", log_args=True)
async def remove_admin_cmd(message: Message) -> None:
"""
Удаляет администратора бота.
Доступно только владельцам бота (OWNER_ID).
Использование: /remadmin <ID>
Пример: /remadmin 123456789
"""
success, result = parse_user_id(message.text, "remadmin")
if not success:
await message.answer(result, parse_mode="HTML")
return
user_id = result
# Проверка: нельзя удалить владельца
if user_id in settings.OWNER_ID:
await message.answer(
"⚠️ <b>Нельзя удалить владельца</b>\n\n"
"Владельцы имеют права постоянно",
parse_mode="HTML"
)
return
# Проверка: нельзя удалить самого себя (если вы владелец)
if user_id == message.from_user.id:
await message.answer(
"⚠️ <b>Нельзя удалить самого себя</b>",
parse_mode="HTML"
)
return
manager = get_manager()
try:
# Проверяем, является ли администратором
is_admin = await manager.is_admin(user_id)
if not is_admin:
await message.answer(
f"⚠️ Пользователь {format_admin_info(user_id)} не является администратором",
parse_mode="HTML"
)
return
# Удаляем администратора
removed = await manager.remove_admin(user_id=user_id)
if removed:
text = (
f"🗑 <b>Администратор удалён</b>\n\n"
f"👤 ID: {format_admin_info(user_id)}\n"
f"👑 Удалил: {format_admin_info(message.from_user.id, message.from_user.username)}\n\n"
f"⚠️ <i>Пользователь больше не имеет доступа к командам бота</i>"
)
logger.info(
f"Администратор удалён: {user_id} (удалил: {message.from_user.id})",
log_type="ADMIN_MGMT"
)
else:
text = "❌ <b>Ошибка удаления администратора</b>\n\nПопробуйте позже"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления администратора: {e}", log_type="ADMIN_MGMT")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
# ================= СПИСОК АДМИНИСТРАТОРОВ =================
@router.callback_query(F.data == "listadmins:refresh")
@router.message(Command(*COMMANDS.get("listadmins", ["listadmins"]), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="LIST_ADMINS")
async def list_admins_cmd(update: Message | CallbackQuery) -> None:
"""
Показывает список всех администраторов бота.
Доступно только владельцам бота (OWNER_ID).
Использование: /listadmins
"""
# Определяем тип update
if isinstance(update, CallbackQuery):
message = update.message
is_callback = True
else:
message = update
is_callback = False
manager = get_manager()
try:
# Получаем всех админов из БД
db_admins = await manager.repo.get_admins()
# Получаем статистику
stats = await manager.get_stats()
# === ФОРМИРУЕМ ВЫВОД ===
output = "👥 <b>СПИСОК АДМИНИСТРАТОРОВ</b>\n\n"
# Владельцы (OWNER_ID)
output += "👑 <b>Владельцы бота</b> (полные права):\n"
for owner_id in settings.OWNER_ID:
output += f"├─ <code>{owner_id}</code>\n"
output += "\n"
# Администраторы из БД
if db_admins:
output += f"⚙️ <b>Администраторы</b> ({len(db_admins)}):\n"
for admin_id in sorted(db_admins):
output += f"├─ <code>{admin_id}</code>\n"
output += "\n"
output += "📋 <b>Права администраторов:</b>\n"
output += "├─ Управление банвордами\n"
output += "├─ Просмотр статистики\n"
output += "├─ Активация режимов модерации\n"
output += "└─ Все команды бота (кроме управления админами)\n\n"
else:
output += "⚙️ <b>Администраторы:</b>\n"
output += "└─ <i>Нет дополнительных администраторов</i>\n\n"
# Общая статистика
total_admins = len(settings.OWNER_ID) + len(db_admins)
output += f"📊 <b>Итого:</b> {total_admins} администратор(ов)\n\n"
# Команды управления
output += "🔧 <b>Управление:</b>\n"
output += "• /addadmin <code>ID</code> — добавить админа\n"
output += "• /remadmin <code>ID</code> — удалить админа\n\n"
output += "💡 <i>Только владельцы могут управлять администраторами</i>"
# Клавиатура
keyboard = get_refresh_admins_kb()
# Отправка
if is_callback:
await message.edit_text(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)
await update.answer("✅ Список обновлён")
else:
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка получения списка администраторов: {e}", log_type="ADMIN_MGMT")
error_text = "❌ <b>Ошибка загрузки списка</b>\n\nПопробуйте позже"
if is_callback:
await update.answer("❌ Ошибка загрузки", show_alert=True)
else:
await message.answer(error_text, parse_mode="HTML")
# ================= ВСПОМОГАТЕЛЬНЫЕ CALLBACK =================
@router.callback_query(F.data == "admin:help_add")
async def admin_help_add_callback(callback: CallbackQuery) -> None:
"""Показывает помощь по добавлению админа"""
text = (
" <b>Как добавить администратора?</b>\n\n"
"1⃣ Узнайте Telegram ID пользователя\n"
" • Используйте бота @userinfobot\n"
" • Или попросите пользователя написать /start\n\n"
"2⃣ Выполните команду:\n"
" <code>/addadmin ID</code>\n\n"
"Пример:\n"
"<code>/addadmin 123456789</code>"
)
await callback.answer()
await callback.message.answer(text, parse_mode="HTML")
@router.message(Command(*COMMANDS.get("adminhelp", ["adminhelp"]), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
async def admin_help_cmd(message: Message) -> None:
"""
Показывает подробную справку по управлению администраторами.
Использование: /adminhelp
"""
text = (
"👥 <b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ</b>\n\n"
"🔐 <b>Уровни доступа:</b>\n\n"
"👑 <b>Владельцы</b> (OWNER_ID):\n"
"├─ Все права администратора\n"
"├─ Управление другими админами\n"
"└─ Указываются в конфигурации\n\n"
"⚙️ <b>Администраторы:</b>\n"
"├─ Управление банвордами\n"
"├─ Просмотр статистики\n"
"├─ Активация режимов модерации\n"
"└─ НЕ могут управлять админами\n\n"
"📝 <b>Команды:</b>\n"
"• /listadmins — список всех админов\n"
"• /addadmin <code>ID</code> — добавить админа\n"
"• /remadmin <code>ID</code> — удалить админа\n\n"
"💡 <b>Как узнать ID пользователя?</b>\n"
"• Используйте бота @userinfobot\n"
"• Попросите пользователя написать боту\n"
"• ID отображается в логах бота\n\n"
"⚠️ <b>Важно:</b>\n"
"├─ Нельзя удалить владельца\n"
"├─ Нельзя удалить самого себя\n"
"└─ Все действия логируются"
)
await message.answer(text, parse_mode="HTML")
@router.message(Command(*COMMANDS.get("checkadmin", ["checkadmin"]), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="CHECK_ADMIN")
async def check_admin_cmd(message: Message) -> None:
"""
Проверяет, является ли пользователь администратором.
Использование: /checkadmin <ID>
"""
success, result = parse_user_id(message.text, "checkadmin")
if not success:
await message.answer(result, parse_mode="HTML")
return
user_id = result
manager = get_manager()
try:
# Проверяем статус
is_owner = user_id in settings.OWNER_ID
is_db_admin = await manager.is_admin(user_id)
text = f"🔍 <b>Проверка пользователя</b>\n\n"
text += f"👤 ID: <code>{user_id}</code>\n\n"
if is_owner:
text += "👑 Статус: <b>Владелец бота</b>\n"
text += "✅ Полные права администратора\n"
text += "✅ Может управлять админами"
elif is_db_admin:
text += "⚙️ Статус: <b>Администратор</b>\n"
text += "✅ Доступ к командам бота\n"
text += "Не может управлять админами"
else:
text += "👤 Статус: <b>Обычный пользователь</b>\n"
text += "❌ Нет прав администратора\n\n"
text += f"Добавить в админы: <code>/addadmin {user_id}</code>"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка проверки администратора: {e}", log_type="ADMIN_MGMT")
await message.answer("❌ <b>Ошибка проверки</b>", parse_mode="HTML")

View File

@@ -0,0 +1,435 @@
"""
Обработчики команд режима антиконфликта
"""
from datetime import datetime
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from database import get_manager
from database.models import BanWordType
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
router: Router = Router(name="conflict_mode_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def parse_conflict_args(text: str, command: str, need_minutes: bool = False) -> tuple[bool, str | list]:
"""
Парсит аргументы команды для конфликтного режима.
Args:
text: Полный текст сообщения
command: Название команды
need_minutes: Требуется ли параметр минут
Returns:
(success, result): result это либо список аргументов, либо текст ошибки
"""
parts = text.split(maxsplit=2 if need_minutes else 1)
min_args = 1 if need_minutes else 1
if len(parts) < min_args + 1:
if need_minutes:
return False, f"❌ Использование: <code>/{command} [минуты]</code>"
else:
return False, f"❌ Использование: <code>/{command} [слово]</code>"
args = parts[1:]
# Валидация слова
if not need_minutes:
if len(args[0]) < 2:
return False, "❌ Слово должно содержать минимум 2 символа"
if len(args[0]) > 100:
return False, "❌ Слово слишком длинное (максимум 100 символов)"
return True, args
def format_time_str(minutes: int) -> str:
"""Форматирует время в читабельный формат"""
if minutes < 60:
return f"{minutes} мин"
elif minutes < 1440:
hours = minutes // 60
mins = minutes % 60
return f"{hours}ч {mins}м" if mins else f"{hours}ч"
else:
days = minutes // 1440
hours = (minutes % 1440) // 60
return f"{days}д {hours}ч" if hours else f"{days}д"
def format_datetime(dt: datetime) -> str:
"""Форматирует datetime в читабельный формат"""
return dt.strftime("%d.%m.%Y %H:%M:%S")
# ================= ДОБАВЛЕНИЕ КОНФЛИКТНЫХ СЛОВ =================
@router.message(
Command(*COMMANDS.get("addconflictword", ["addconflictword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="ADD_CONFLICT_WORD", log_args=True)
async def add_conflict_word_cmd(message: Message) -> None:
"""
Добавляет конфликтное слово-подстроку.
Конфликтные слова работают только в режиме /stopconflict.
Использование: /addconflictword <слово>
"""
success, result = parse_conflict_args(message.text, "addconflictword", need_minutes=False)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
added = await manager.add_banword(
word=word,
word_type=BanWordType.CONFLICT_SUBSTRING,
added_by=message.from_user.id,
reason="Конфликтное слово"
)
if added:
text = (
f"✅ <b>Конфликтное слово добавлено</b>\n\n"
f"📝 Слово: <code>{word}</code>\n"
f"🔍 Тип: подстрока\n\n"
f"⚔️ <i>Будет работать только в режиме антиконфликта</i>\n"
f"Активируйте: <code>/stopconflict [минуты]</code>"
)
else:
text = f"⚠️ Конфликтное слово <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления конфликтного слова: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(
Command(*COMMANDS.get("addconflictlemma", ["addconflictlemma"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="ADD_CONFLICT_LEMMA", log_args=True)
async def add_conflict_lemma_cmd(message: Message) -> None:
"""
Добавляет конфликтную лемму.
Конфликтные леммы работают только в режиме /stopconflict.
Использование: /addconflictlemma <слово>
"""
success, result = parse_conflict_args(message.text, "addconflictlemma", need_minutes=False)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
added = await manager.add_banword(
word=word,
word_type=BanWordType.CONFLICT_LEMMA,
added_by=message.from_user.id,
reason="Конфликтная лемма"
)
if added:
text = (
f"✅ <b>Конфликтная лемма добавлена</b>\n\n"
f"🔤 Слово: <code>{word}</code>\n"
f"🔍 Тип: лемма (все формы слова)\n\n"
f"⚔️ <i>Будет работать только в режиме антиконфликта</i>\n"
f"Активируйте: <code>/stopconflict [минуты]</code>"
)
else:
text = f"⚠️ Конфликтная лемма <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления конфликтной леммы: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
# ================= УДАЛЕНИЕ КОНФЛИКТНЫХ СЛОВ =================
@router.message(
Command(*COMMANDS.get("remconflictword", ["remconflictword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="REMOVE_CONFLICT_WORD", log_args=True)
async def remove_conflict_word_cmd(message: Message) -> None:
"""
Удаляет конфликтное слово-подстроку.
Использование: /remconflictword <слово>
"""
success, result = parse_conflict_args(message.text, "remconflictword", need_minutes=False)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_banword(
word=word,
word_type=BanWordType.CONFLICT_SUBSTRING
)
if removed:
text = f"🗑 <b>Конфликтное слово удалено</b>\n\n📝 Слово: <code>{word}</code>"
else:
text = f"⚠️ Конфликтное слово <code>{word}</code> не найдено"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления конфликтного слова: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(
Command(*COMMANDS.get("remconflictlemma", ["remconflictlemma"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="REMOVE_CONFLICT_LEMMA", log_args=True)
async def remove_conflict_lemma_cmd(message: Message) -> None:
"""
Удаляет конфликтную лемму.
Использование: /remconflictlemma <слово>
"""
success, result = parse_conflict_args(message.text, "remconflictlemma", need_minutes=False)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_banword(
word=word,
word_type=BanWordType.CONFLICT_LEMMA
)
if removed:
text = f"🗑 <b>Конфликтная лемма удалена</b>\n\n🔤 Слово: <code>{word}</code>"
else:
text = f"⚠️ Конфликтная лемма <code>{word}</code> не найдена"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления конфликтной леммы: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
# ================= УПРАВЛЕНИЕ РЕЖИМОМ АНТИКОНФЛИКТА =================
@router.message(Command(*COMMANDS.get("stopconflict", ["stopconflict"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="START_CONFLICT_MODE", log_args=True)
async def start_conflict_mode_cmd(message: Message) -> None:
"""
Активирует режим антиконфликта на указанное время.
В этом режиме работают только конфликтные слова/леммы.
Обычные банворды временно отключаются.
Использование: /stopconflict <минуты>
Пример: /stopconflict 30
"""
success, result = parse_conflict_args(message.text, "stopconflict", need_minutes=True)
if not success:
await message.answer(result, parse_mode="HTML")
return
# Валидация минут
try:
minutes = int(result[0])
if minutes < 1 or minutes > 10080: # Максимум неделя
await message.answer(
"❌ Время должно быть от 1 минуты до 10080 минут (7 дней)",
parse_mode="HTML"
)
return
except ValueError:
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
return
manager = get_manager()
try:
# Получаем статистику конфликтных слов
data = await manager.get_all_words_list()
conflict_words_count = len(data.get('conflict_substring', set()))
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
total_conflict = conflict_words_count + conflict_lemmas_count
if total_conflict == 0:
await message.answer(
"⚠️ <b>Нет конфликтных слов</b>\n\n"
"Сначала добавьте конфликтные слова:\n"
"• <code>/addconflictword [слово]</code>\n"
"• <code>/addconflictlemma [слово]</code>",
parse_mode="HTML"
)
return
# Активируем режим
expires_at = await manager.set_conflict_mode(minutes)
time_str = format_time_str(minutes)
expires_str = format_datetime(expires_at)
text = (
f"⚔️ <b>РЕЖИМ АНТИКОНФЛИКТА АКТИВИРОВАН</b>\n\n"
f"⏱ Длительность: {time_str}\n"
f"🕐 Окончание: {expires_str}\n\n"
f"📊 Активные правила:\n"
f"├─ Конфликтные слова: <code>{conflict_words_count}</code>\n"
f"└─ Конфликтные леммы: <code>{conflict_lemmas_count}</code>\n\n"
f"⚠️ <i>Обычные банворды временно отключены</i>\n"
f"Отключить режим: /unstopconflict"
)
await message.answer(text, parse_mode="HTML")
logger.info(
f"Режим антиконфликта активирован на {minutes} мин "
f"(конфликтных правил: {total_conflict})",
log_type="CONFLICT"
)
except Exception as e:
logger.error(f"Ошибка активации режима антиконфликта: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка активации режима</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("unstopconflict", ["unstopconflict"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="STOP_CONFLICT_MODE")
async def stop_conflict_mode_cmd(message: Message) -> None:
"""
Отключает режим антиконфликта.
Использование: /unstopconflict
"""
manager = get_manager()
try:
# Проверяем, активен ли режим
is_active = await manager.is_conflict_active()
if not is_active:
await message.answer(
"⚠️ <b>Режим антиконфликта не активен</b>\n\n"
"Активируйте: <code>/stopconflict [минуты]</code>",
parse_mode="HTML"
)
return
# Отключаем режим
await manager.disable_conflict_mode()
text = (
f"✅ <b>Режим антиконфликта отключен</b>\n\n"
f"🔄 Обычные банворды снова активны\n"
f"⚔️ Конфликтные слова деактивированы"
)
await message.answer(text, parse_mode="HTML")
logger.info("Режим антиконфликта отключён", log_type="CONFLICT")
except Exception as e:
logger.error(f"Ошибка отключения режима антиконфликта: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка отключения режима</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("conflictstatus", ["conflictstatus"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="CONFLICT_STATUS")
async def conflict_status_cmd(message: Message) -> None:
"""
Показывает статус режима антиконфликта.
Использование: /conflictstatus
"""
manager = get_manager()
try:
# Проверяем активность режима
is_active = await manager.is_conflict_active()
# Получаем статистику
data = await manager.get_all_words_list()
conflict_words_count = len(data.get('conflict_substring', set()))
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
total_conflict = conflict_words_count + conflict_lemmas_count
if is_active:
# Режим активен - показываем детали
conflict_until_str = await manager.repo.get_setting("conflict_until")
conflict_until = float(conflict_until_str)
expires_at = datetime.fromtimestamp(conflict_until)
now = datetime.now()
time_left_seconds = (expires_at - now).total_seconds()
time_left_minutes = int(time_left_seconds / 60)
text = (
f"⚔️ <b>РЕЖИМ АНТИКОНФЛИКТА АКТИВЕН</b>\n\n"
f"⏱ Осталось: {format_time_str(time_left_minutes)}\n"
f"🕐 Окончание: {format_datetime(expires_at)}\n\n"
f"📊 Активные правила:\n"
f"├─ Конфликтные слова: <code>{conflict_words_count}</code>\n"
f"└─ Конфликтные леммы: <code>{conflict_lemmas_count}</code>\n\n"
f"⚠️ <i>Обычные банворды отключены</i>\n"
f"Отключить: /unstopconflict"
)
else:
# Режим не активен
text = (
f"💤 <b>Режим антиконфликта НЕ активен</b>\n\n"
f"📊 Конфликтных правил в базе:\n"
f"├─ Слова: <code>{conflict_words_count}</code>\n"
f"└─ Леммы: <code>{conflict_lemmas_count}</code>\n\n"
)
if total_conflict > 0:
text += f"Активировать: <code>/stopconflict [минуты]</code>"
else:
text += (
f"⚠️ <i>Нет конфликтных слов</i>\n"
f"Добавьте:\n"
f"• <code>/addconflictword [слово]</code>\n"
f"• <code>/addconflictlemma [слово]</code>"
)
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка получения статуса режима: {e}", log_type="CONFLICT")
await message.answer("❌ <b>Ошибка получения статуса</b>", parse_mode="HTML")

View File

@@ -0,0 +1,215 @@
"""
Обработчик команды /emoji для извлечения ID премиум эмодзи
"""
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
router: Router = Router(name="emoji_extractor_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def extract_custom_emojis(message: Message) -> list[dict]:
"""
Извлекает все кастомные эмодзи из сообщения.
Args:
message: Сообщение для анализа
Returns:
Список словарей с информацией об эмодзи
"""
if not message.entities and not message.caption_entities:
return []
# Определяем текст и entities
text = message.text or message.caption
entities = message.entities or message.caption_entities
if not text or not entities:
return []
custom_emojis = []
for entity in entities:
if entity.type == "custom_emoji":
# Извлекаем символ эмодзи
emoji_char = text[entity.offset:entity.offset + entity.length]
custom_emojis.append({
"char": emoji_char,
"id": entity.custom_emoji_id,
"offset": entity.offset
})
return custom_emojis
def format_emoji_html(emoji_char: str, emoji_id: str) -> str:
"""
Форматирует эмодзи в HTML-тег.
Args:
emoji_char: Символ эмодзи (fallback)
emoji_id: ID кастомного эмодзи
Returns:
HTML-строка
"""
return f'<tg-emoji emoji-id="{emoji_id}">{emoji_char}</tg-emoji>'
def escape_html(text: str) -> str:
"""Экранирует HTML символы"""
return (
text.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
# ================= КОМАНДА /EMOJI =================
@router.message(
Command(*COMMANDS.get("emoji", ["emoji"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin()
)
async def emoji_extractor_cmd(message: Message) -> None:
"""
Извлекает кастомные эмодзи из сообщения.
Доступно только администраторам.
Использование: /emoji (в ответ на сообщение)
"""
# Проверяем, что команда в ответ на сообщение
if not message.reply_to_message:
await message.answer(
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
"📝 Как использовать:\n"
"1. Ответьте на сообщение с премиум эмодзи\n"
"2. Напишите <code>/emoji</code>\n\n"
"💡 <i>Бот извлечёт все кастомные эмодзи и покажет HTML-код</i>",
parse_mode="HTML"
)
return
replied_message = message.reply_to_message
# Извлекаем кастомные эмодзи
custom_emojis = extract_custom_emojis(replied_message)
if not custom_emojis:
# Нет кастомных эмодзи
await message.answer(
"⚠️ <b>Кастомные эмодзи не найдены</b>\n\n"
"В этом сообщении нет премиум эмодзи.\n\n"
"💡 <i>Попробуйте ответить на сообщение с анимированными эмодзи</i>",
parse_mode="HTML"
)
return
# === ФОРМИРУЕМ ОТВЕТ ===
output = f"✨ <b>НАЙДЕНО ЭМОДЗИ: {len(custom_emojis)}</b>\n\n"
for idx, emoji_data in enumerate(custom_emojis, 1):
emoji_char = emoji_data["char"]
emoji_id = emoji_data["id"]
output += f"<b>{idx}.</b> Эмодзи: {emoji_char}\n"
output += f"📋 <b>ID:</b> <code>{emoji_id}</code>\n\n"
# HTML-код (экранированный для отображения)
html_code = format_emoji_html(emoji_char, emoji_id)
html_escaped = escape_html(html_code)
output += f"📝 <b>HTML-код:</b>\n"
output += f"<code>{html_escaped}</code>\n\n"
# Пример использования
output += f"🎨 <b>Превью:</b> {html_code}\n"
if idx < len(custom_emojis):
output += "\n" + "" * 30 + "\n\n"
output += "💡 <i>Скопируйте HTML-код и используйте в своих сообщениях</i>"
# Создаём клавиатуру
ikb = InlineKeyboardBuilder()
ikb.button(text="✖️ Закрыть", callback_data="emoji_close")
# Отправляем
try:
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=ikb.as_markup()
)
logger.info(
f"Извлечено {len(custom_emojis)} кастомных эмодзи админом {message.from_user.id}",
log_type="EMOJI_EXTRACT"
)
except Exception as e:
logger.error(f"Ошибка отправки эмодзи: {e}", log_type="ERROR")
await message.answer(
"❌ <b>Ошибка извлечения эмодзи</b>\n\n"
"Попробуйте позже или обратитесь к разработчику.",
parse_mode="HTML"
)
# ================= ОБРАБОТЧИК КНОПКИ ЗАКРЫТИЯ =================
@router.callback_query(lambda c: c.data == "emoji_close", IsAdmin())
async def emoji_close_callback(callback) -> None:
"""Закрывает сообщение с эмодзи"""
try:
await callback.message.delete()
await callback.answer("✅ Закрыто")
except Exception as e:
logger.error(f"Ошибка удаления сообщения с эмодзи: {e}", log_type="ERROR")
await callback.answer("Не удалось удалить", show_alert=True)
# ================= ДОПОЛНИТЕЛЬНАЯ КОМАНДА /EMOJIHELP =================
@router.message(
Command(*COMMANDS.get("emojihelp", ["emojihelp"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin()
)
async def emoji_help_cmd(message: Message) -> None:
"""
Справка по работе с кастомными эмодзи.
"""
text = (
"🎨 <b>РАБОТА С КАСТОМНЫМИ ЭМОДЗИ</b>\n\n"
"📝 <b>Команда /emoji</b>\n"
"Извлекает ID премиум эмодзи из сообщения\n\n"
"🔧 <b>Как использовать:</b>\n"
"1⃣ Ответьте на сообщение с эмодзи\n"
"2⃣ Напишите <code>/emoji</code>\n"
"3⃣ Скопируйте HTML-код\n\n"
"💻 <b>Формат HTML-кода:</b>\n"
"<code>&lt;tg-emoji emoji-id=\"ID\"&gt;fallback&lt;/tg-emoji&gt;</code>\n\n"
"📌 <b>Пример использования в коде:</b>\n"
"<code>text = 'Привет &lt;tg-emoji emoji-id=\"5368324170671202286\"&gt;👍&lt;/tg-emoji&gt;'\n"
"await message.answer(text, parse_mode=\"HTML\")</code>\n\n"
"⚠️ <b>Важно:</b>\n"
"├─ Используйте <code>parse_mode=\"HTML\"</code>\n"
"├─ Пользователи без Premium видят fallback\n"
"└─ Работает только с кастомными эмодзи\n\n"
"💡 <i>Попробуйте отправить эмодзи и ответить командой /emoji</i>"
)
await message.answer(text, parse_mode="HTML")

View File

@@ -0,0 +1,221 @@
"""
Обработчик команды /id для получения информации о пользователе
"""
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from configs import settings, COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
router: Router = Router(name="user_id_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def get_close_keyboard():
"""Создаёт клавиатуру с кнопкой закрытия"""
ikb = InlineKeyboardBuilder()
ikb.button(text="✖️ Закрыть", callback_data="id_close")
return ikb.as_markup()
# ================= КОМАНДА /ID =================
@router.message(Command(*COMMANDS.get("id", ["id"]), prefix=settings.PREFIX, ignore_case=True))
async def id_cmd(message: Message) -> None:
"""
Показывает информацию о вашем Telegram аккаунте.
Доступно всем пользователям.
Использование: /id
"""
user = message.from_user
if not user:
await message.answer("Не удалось получить информацию о пользователе")
return
# === ФОРМИРУЕМ ИНФОРМАЦИЮ ===
output = "👤 <b>ИНФОРМАЦИЯ О ВАС</b>\n\n"
# Имя
full_name_parts = []
if user.first_name:
full_name_parts.append(user.first_name)
if user.last_name:
full_name_parts.append(user.last_name)
full_name = " ".join(full_name_parts) if full_name_parts else "Не указано"
output += f"📝 <b>Имя:</b> {full_name}\n"
# Username
if user.username:
output += f"🔗 <b>Username:</b> @{user.username}\n"
else:
output += f"🔗 <b>Username:</b> <i>не установлен</i>\n"
# ID
output += f"🆔 <b>ID:</b> <code>{user.id}</code>\n\n"
# Тип аккаунта
if user.is_bot:
output += f"🤖 <b>Тип:</b> Бот\n"
elif user.is_premium:
output += f"⭐️ <b>Тип:</b> Premium пользователь\n"
else:
output += f"👥 <b>Тип:</b> Обычный пользователь\n"
# Дополнительная информация
output += "\n📊 <b>Дополнительно:</b>\n"
# Язык
if user.language_code:
language_names = {
'ru': '🇷🇺 Русский',
'en': '🇬🇧 English',
'uk': '🇺🇦 Українська',
'de': '🇩🇪 Deutsch',
'es': '🇪🇸 Español',
'fr': '🇫🇷 Français',
'it': '🇮🇹 Italiano',
'pt': '🇵🇹 Português',
}
language = language_names.get(user.language_code, f"🌐 {user.language_code.upper()}")
output += f"├─ Язык: {language}\n"
# Информация о чате
if message.chat.type == "private":
output += f"├─ Чат: 💬 Личные сообщения\n"
else:
chat_title = message.chat.title or "Без названия"
chat_types = {
"group": "👥 Группа",
"supergroup": "👥 Супергруппа",
"channel": "📢 Канал"
}
chat_type = chat_types.get(message.chat.type, "💬 Чат")
output += f"├─ Чат: {chat_type}\n"
output += f"├─ Название: {chat_title}\n"
output += f"├─ Chat ID: <code>{message.chat.id}</code>\n"
# Получаем количество участников (только для групп)
try:
member_count = await message.bot.get_chat_member_count(message.chat.id)
output += f"├─ Участников: {member_count}\n"
except Exception as e:
logger.debug(f"Не удалось получить количество участников: {e}", log_type="USER_ID")
# Message ID
output += f"└─ Message ID: <code>{message.message_id}</code>\n\n"
# Подсказка
output += "💡 <i>Эту информацию видите только вы</i>"
# Клавиатура
keyboard = get_close_keyboard()
# Отправляем
try:
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)
logger.debug(f"Команда /id от пользователя {user.id}", log_type="USER_ID")
except Exception as e:
logger.error(f"Ошибка отправки информации о пользователе: {e}", log_type="ERROR")
await message.answer("❌ Произошла ошибка при получении информации")
# ================= ОБРАБОТЧИК КНОПКИ ЗАКРЫТИЯ =================
@router.callback_query(F.data == "id_close")
async def id_close_callback(callback: CallbackQuery) -> None:
"""Закрывает (удаляет) сообщение с информацией"""
try:
await callback.message.delete()
await callback.answer("✅ Закрыто")
except Exception as e:
logger.error(f"Ошибка удаления сообщения ID: {e}", log_type="ERROR")
await callback.answer("Не удалось удалить сообщение", show_alert=True)
# ================= КОМАНДА /MYID (АЛЬТЕРНАТИВА) =================
@router.message(Command(*COMMANDS.get("myid", ["myid"]), prefix=settings.PREFIX, ignore_case=True))
async def myid_cmd(message: Message) -> None:
"""
Быстрый просмотр вашего ID.
Использование: /myid
"""
user = message.from_user
if not user:
await message.answer("Не удалось получить ID")
return
# Короткий ответ
text = f"🆔 Ваш ID: <code>{user.id}</code>"
if user.username:
text += f"\n🔗 Username: @{user.username}"
await message.answer(text, parse_mode="HTML")
# ================= КОМАНДА /CHATID =================
@router.message(Command(*COMMANDS.get("chatid", ["chatid"]), prefix=settings.PREFIX, ignore_case=True))
async def chatid_cmd(message: Message) -> None:
"""
Показывает ID текущего чата.
Использование: /chatid
"""
chat = message.chat
output = "💬 <b>ИНФОРМАЦИЯ О ЧАТЕ</b>\n\n"
# Тип чата
chat_types = {
"private": "💬 Личные сообщения",
"group": "👥 Группа",
"supergroup": "👥 Супергруппа",
"channel": "📢 Канал"
}
chat_type = chat_types.get(chat.type, "💬 Чат")
output += f"📝 <b>Тип:</b> {chat_type}\n"
if chat.title:
output += f"📌 <b>Название:</b> {chat.title}\n"
if chat.username:
output += f"🔗 <b>Username:</b> @{chat.username}\n"
output += f"🆔 <b>Chat ID:</b> <code>{chat.id}</code>\n"
# Дополнительная информация для групп
if chat.type in ["group", "supergroup"]:
try:
member_count = await message.bot.get_chat_member_count(chat.id)
output += f"👥 <b>Участников:</b> {member_count}\n"
except Exception as e:
logger.debug(f"Не удалось получить количество участников: {e}", log_type="USER_ID")
keyboard = get_close_keyboard()
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)

View File

@@ -0,0 +1,238 @@
"""
Обработчик команды /listwords - отображение всех правил модерации
"""
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from database import get_manager
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
CMD: str = "list"
router: Router = Router(name="listwords_cmd_router")
def get_refresh_kb(page: int = 0):
"""Клавиатура с кнопкой обновления"""
ikb = InlineKeyboardBuilder()
ikb.button(text="🔄 Обновить", callback_data=f"listwords:refresh:{page}")
ikb.button(text="📊 Статистика", callback_data="stats")
ikb.adjust(2)
return ikb.as_markup()
async def format_banwords_list(page: int = 0) -> str:
"""
Форматирует список всех банвордов с разбивкой по типам.
Args:
page: Номер страницы (для будущей пагинации)
Returns:
Отформатированная строка со всеми правилами
"""
manager = get_manager()
# Получаем все данные из БД
try:
# Используем существующий метод get_all_words_list()
data = await manager.get_all_words_list()
stats = await manager.get_stats()
# Извлекаем данные из словаря
permanent_words = list(data.get('substring', set()))
permanent_lemmas = list(data.get('lemma', set()))
permanent_parts = list(data.get('part', set()))
temp_words = list(data.get('temp_substring', set()))
temp_lemmas = list(data.get('temp_lemma', set()))
conflict_words = list(data.get('conflict_substring', set()))
conflict_lemmas = list(data.get('conflict_lemma', set()))
exceptions = list(data.get('whitelist', set()))
except Exception as e:
logger.error(f"Ошибка получения данных из БД: {e}", log_type="LISTWORDS")
return "❌ <b>Ошибка загрузки данных из базы</b>"
# === ФОРМИРУЕМ ВЫВОД ===
output = "📋 <b>СПИСОК ПРАВИЛ МОДЕРАЦИИ</b>\n\n"
# Статистика
total_count = (
len(permanent_words) + len(permanent_lemmas) + len(permanent_parts) +
len(temp_words) + len(temp_lemmas) +
len(conflict_words) + len(conflict_lemmas)
)
output += f"📊 <b>Общая статистика:</b>\n"
output += f"├─ Всего правил: <code>{total_count}</code>\n"
output += f"├─ Исключений: <code>{len(exceptions)}</code>\n"
output += f"├─ Удалений за всё время: <code>{stats.get('total_deletions', 0)}</code>\n"
output += f"└─ Администраторов: <code>{stats.get('admins', 0)}</code>\n\n"
# === ПОСТОЯННЫЕ ПРАВИЛА ===
if permanent_words or permanent_lemmas or permanent_parts:
output += "🔴 <b>ПОСТОЯННЫЕ ПРАВИЛА:</b>\n\n"
if permanent_words:
output += f"📝 <b>Подстроки</b> ({len(permanent_words)}):\n"
words_str = ', '.join([f"<code>{w}</code>" for w in sorted(permanent_words)[:20]])
if len(permanent_words) > 20:
words_str += f" ... <i>(+{len(permanent_words) - 20} ещё)</i>"
output += f"{words_str}\n\n"
if permanent_lemmas:
output += f"🔤 <b>Леммы</b> ({len(permanent_lemmas)}):\n"
lemmas_str = ', '.join([f"<code>{w}</code>" for w in sorted(permanent_lemmas)[:20]])
if len(permanent_lemmas) > 20:
lemmas_str += f" ... <i>(+{len(permanent_lemmas) - 20} ещё)</i>"
output += f"{lemmas_str}\n\n"
if permanent_parts:
output += f"🧩 <b>Части</b> ({len(permanent_parts)}):\n"
parts_str = ', '.join([f"<code>{w}</code>" for w in sorted(permanent_parts)[:20]])
if len(permanent_parts) > 20:
parts_str += f" ... <i>(+{len(permanent_parts) - 20} ещё)</i>"
output += f"{parts_str}\n\n"
# === ВРЕМЕННЫЕ ПРАВИЛА ===
if temp_words or temp_lemmas:
output += "⏱ <b>ВРЕМЕННЫЕ ПРАВИЛА:</b>\n\n"
if temp_words:
output += f"📝 <b>Временные подстроки</b> ({len(temp_words)}):\n"
# Для временных слов нужна дополнительная информация о времени истечения
# Пока просто выводим список
words_str = ', '.join([f"<code>{w}</code>" for w in sorted(temp_words)[:15]])
if len(temp_words) > 15:
words_str += f" ... <i>(+{len(temp_words) - 15} ещё)</i>"
output += f"{words_str}\n\n"
if temp_lemmas:
output += f"🔤 <b>Временные леммы</b> ({len(temp_lemmas)}):\n"
lemmas_str = ', '.join([f"<code>{w}</code>" for w in sorted(temp_lemmas)[:15]])
if len(temp_lemmas) > 15:
lemmas_str += f" ... <i>(+{len(temp_lemmas) - 15} ещё)</i>"
output += f"{lemmas_str}\n\n"
# === КОНФЛИКТНЫЕ ПРАВИЛА ===
if conflict_words or conflict_lemmas:
output += "⚔️ <b>КОНФЛИКТНЫЕ ПРАВИЛА:</b>\n"
output += "<i>(работают только в режиме /stopconflict)</i>\n\n"
if conflict_words:
output += f"📝 <b>Конфликтные слова</b> ({len(conflict_words)}):\n"
words_str = ', '.join([f"<code>{w}</code>" for w in sorted(conflict_words)[:15]])
if len(conflict_words) > 15:
words_str += f" ... <i>(+{len(conflict_words) - 15} ещё)</i>"
output += f"{words_str}\n\n"
if conflict_lemmas:
output += f"🔤 <b>Конфликтные леммы</b> ({len(conflict_lemmas)}):\n"
lemmas_str = ', '.join([f"<code>{w}</code>" for w in sorted(conflict_lemmas)[:15]])
if len(conflict_lemmas) > 15:
lemmas_str += f" ... <i>(+{len(conflict_lemmas) - 15} ещё)</i>"
output += f"{lemmas_str}\n\n"
# === ИСКЛЮЧЕНИЯ (WHITELIST) ===
if exceptions:
output += f"✅ <b>ИСКЛЮЧЕНИЯ</b> ({len(exceptions)}):\n"
exc_str = ', '.join([f"<code>{exceptions}</code>" for w in sorted(exceptions)[:15]])
if len(exceptions) > 15:
exc_str += f" ... <i>(+{len(exceptions) - 15} ещё)</i>"
output += f"{exc_str}\n\n"
# === АКТИВНЫЕ РЕЖИМЫ ===
active_modes = []
if await manager.is_silence_active():
active_modes.append("🔇 Режим тишины")
if await manager.is_conflict_active():
active_modes.append("⚔️ Режим антиконфликта")
if active_modes:
output += "🔴 <b>АКТИВНЫЕ РЕЖИМЫ:</b>\n"
for mode in active_modes:
output += f"{mode}\n"
output += "\n"
# === ПУСТОЙ СПИСОК ===
if total_count == 0:
output = (
"📋 <b>СПИСОК ПРАВИЛ МОДЕРАЦИИ</b>\n\n"
"⚠️ <i>Правила модерации не настроены</i>\n\n"
"Используйте команды добавления:\n"
"• /addword — добавить подстроку\n"
"• /addlemma — добавить лемму\n"
"• /addpart — добавить часть\n\n"
"📖 Подробнее: /start"
)
# Ограничение длины (Telegram limit 4096)
if len(output) > 4000:
output = output[:3950] + "\n\n<i>... список обрезан, слишком много правил</i>"
return output
@router.callback_query(F.data.startswith("listwords:refresh"))
@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="LISTWORDS_COMMAND")
async def listwords_cmd(update: Message | CallbackQuery) -> None:
"""
Обработчик команды /listwords.
Отображает список всех правил модерации с разбивкой по категориям.
Доступно только администраторам.
Args:
update: Message или CallbackQuery
"""
# Определяем тип update
if isinstance(update, CallbackQuery):
message = update.message
is_callback = True
# Извлекаем номер страницы из callback_data
try:
page = int(update.data.split(":")[-1])
except:
page = 0
else:
message = update
is_callback = False
page = 0
# Формируем список
try:
text = await format_banwords_list(page)
keyboard = get_refresh_kb(page)
if is_callback:
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=keyboard
)
await update.answer("✅ Список обновлён")
else:
await message.answer(
text=text,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка отправки списка банвордов: {e}", log_type="LISTWORDS")
error_text = "❌ <b>Ошибка загрузки списка</b>\n\nПопробуйте позже"
if is_callback:
await update.answer("❌ Ошибка загрузки", show_alert=True)
else:
await message.answer(error_text, parse_mode="HTML")

View File

@@ -0,0 +1,118 @@
"""
Обработчики callback-кнопок уведомлений о спаме
"""
from aiogram import Router, F
from aiogram.types import CallbackQuery
from aiogram.exceptions import TelegramBadRequest
from bot.filters.admin import IsAdmin
from database import get_manager
from middleware.loggers import logger
__all__ = ("router",)
router: Router = Router(name="spam_notifications_router")
# ================= ЗАКРЫТИЕ УВЕДОМЛЕНИЯ =================
@router.callback_query(F.data == "spam_close", IsAdmin())
async def spam_close_callback(callback: CallbackQuery) -> None:
"""
Закрывает (удаляет) уведомление о спаме.
"""
try:
await callback.message.delete()
await callback.answer("✅ Уведомление закрыто")
logger.debug(
f"Уведомление о спаме закрыто админом {callback.from_user.id}",
log_type="SPAM_NOTIFICATION"
)
except TelegramBadRequest as e:
logger.error(f"Ошибка удаления уведомления: {e}", log_type="ERROR")
await callback.answer("Не удалось удалить уведомление", show_alert=True)
# ================= БАН ПОЛЬЗОВАТЕЛЯ =================
@router.callback_query(F.data.startswith("spam_ban:"), IsAdmin())
async def spam_ban_callback(callback: CallbackQuery) -> None:
"""
Банит пользователя прямо из уведомления.
"""
try:
# Парсим данные: spam_ban:user_id:chat_id
parts = callback.data.split(":")
user_id = int(parts[1])
chat_id = int(parts[2])
# Баним пользователя
try:
await callback.bot.ban_chat_member(
chat_id=chat_id,
user_id=user_id
)
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n🔨 <b>Пользователь забанен</b> (@{callback.from_user.username or callback.from_user.id})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
await callback.answer("✅ Пользователь забанен", show_alert=True)
logger.info(
f"Пользователь {user_id} забанен админом {callback.from_user.id} через уведомление о спаме",
log_type="SPAM_BAN"
)
except TelegramBadRequest as e:
await callback.answer(f"❌ Ошибка бана: {str(e)}", show_alert=True)
except Exception as e:
logger.error(f"Ошибка обработки бана из уведомления: {e}", log_type="ERROR")
await callback.answer("❌ Ошибка выполнения", show_alert=True)
# ================= СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ =================
@router.callback_query(F.data.startswith("spam_stats:"), IsAdmin())
async def spam_stats_callback(callback: CallbackQuery) -> None:
"""
Показывает статистику пользователя.
"""
try:
# Парсим данные: spam_stats:user_id
parts = callback.data.split(":")
user_id = int(parts[1])
manager = get_manager()
# Получаем статистику
spam_count = await manager.get_user_spam_count(user_id)
recent_spam = await manager.get_spam_stats(limit=5, user_id=user_id)
# Формируем текст
text = f"📊 <b>Статистика пользователя</b>\n\n"
text += f"🆔 ID: <code>{user_id}</code>\n"
text += f"🗑 Удалено сообщений: <code>{spam_count}</code>\n\n"
if recent_spam:
text += f"📝 <b>Последние нарушения:</b>\n"
for idx, stat in enumerate(recent_spam, 1):
matched_word = stat.matched_word or "неизвестно"
match_type = stat.match_type or "unknown"
text += f"{idx}. <code>{matched_word}</code> ({match_type})\n"
else:
text += "✅ <i>Нет нарушений</i>"
await callback.answer(text, show_alert=True)
except Exception as e:
logger.error(f"Ошибка получения статистики из уведомления: {e}", log_type="ERROR")
await callback.answer("❌ Ошибка получения статистики", show_alert=True)

View File

@@ -0,0 +1,447 @@
"""
Обработчики команды /report для пользователей
"""
from datetime import datetime
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, User
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.exceptions import TelegramBadRequest
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from database import get_manager
from middleware.loggers import logger
__all__ = ("router",)
router: Router = Router(name="report_router")
# ================= НАСТРОЙКИ =================
# ID чата для отправки репортов (можно вынести в configs)
# Если None, репорты отправляются всем владельцам в ЛС
REPORT_CHAT_ID = getattr(settings, 'REPORT_CHAT_ID', None)
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def format_user(user: User) -> str:
"""
Форматирует информацию о пользователе.
Args:
user: Объект User
Returns:
Отформатированная строка с именем и username
"""
if not user:
return "Unknown User"
# Формируем имя
name_parts = []
if user.first_name:
name_parts.append(user.first_name)
if user.last_name:
name_parts.append(user.last_name)
full_name = " ".join(name_parts) if name_parts else "No Name"
# Добавляем username если есть
if user.username:
return f"{full_name} (@{user.username})"
else:
return full_name
def format_datetime(dt: datetime) -> str:
"""Форматирует datetime"""
return dt.strftime("%d.%m.%Y %H:%M:%S")
def truncate_text(text: str, max_length: int = 200) -> str:
"""Обрезает текст до указанной длины"""
if len(text) <= max_length:
return text
return text[:max_length] + "..."
def get_report_keyboard(
chat_id: int,
message_id: int,
reported_user_id: int,
report_id: str
) -> InlineKeyboardBuilder:
"""
Создает клавиатуру для репорта.
Args:
chat_id: ID чата, где было сообщение
message_id: ID сообщения
reported_user_id: ID пользователя, на которого пожаловались
report_id: Уникальный ID репорта
"""
ikb = InlineKeyboardBuilder()
# Кнопки действий
ikb.button(
text="🚫 Забанить",
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}"
)
ikb.button(
text="🗑 Удалить",
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}"
)
ikb.button(
text="✅ Закрыть",
callback_data=f"report:close:{report_id}"
)
ikb.adjust(2, 1)
return ikb
def generate_report_id() -> str:
"""Генерирует уникальный ID репорта"""
return f"{int(datetime.now().timestamp() * 1000)}"
# ================= КОМАНДА РЕПОРТА =================
@router.message(Command(*COMMANDS.get("report", ["report"]), prefix=settings.PREFIX, ignore_case=True))
async def report_cmd(message: Message) -> None:
"""
Отправляет жалобу на сообщение администраторам.
Доступно всем пользователям.
Использование:
/report — в ответ на сообщение
/report <причина> — в ответ на сообщение с указанием причины
Пример:
/report спам
/report оскорбления
"""
# Проверяем, что команда в ответ на сообщение
if not message.reply_to_message:
await message.answer(
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
"Как использовать:\n"
"1. Ответьте на сообщение нарушителя\n"
"2. Напишите <code>/report</code> или <code>/report причина</code>\n\n"
"Пример: <code>/report спам</code>",
parse_mode="HTML"
)
return
reported_message = message.reply_to_message
reported_user = reported_message.from_user
reporter = message.from_user
# Проверка на None
if not reported_user or not reporter:
await message.answer("❌ <b>Ошибка получения данных пользователя</b>", parse_mode="HTML")
return
# Нельзя пожаловаться на самого себя
if reported_user.id == reporter.id:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на самого себя</b>",
parse_mode="HTML"
)
return
# Нельзя пожаловаться на бота
if reported_user.is_bot:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на бота</b>",
parse_mode="HTML"
)
return
# Нельзя пожаловаться на администратора
manager = get_manager()
is_admin = await manager.is_admin(reported_user.id) or reported_user.id in settings.OWNER_ID
if is_admin:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на администратора</b>",
parse_mode="HTML"
)
return
# Извлекаем причину (опционально)
parts = message.text.split(maxsplit=1)
reason = parts[1] if len(parts) > 1 else "Не указана"
# Генерируем ID репорта
report_id = generate_report_id()
# === ФОРМИРУЕМ СООБЩЕНИЕ РЕПОРТА ===
report_text = "🚨 <b>НОВЫЙ РЕПОРТ</b>\n\n"
# Информация о жалобщике
report_text += f"👤 <b>От:</b> {format_user(reporter)} (<code>{reporter.id}</code>)\n"
# Информация о нарушителе
report_text += f"⚠️ <b>На:</b> {format_user(reported_user)} (<code>{reported_user.id}</code>)\n\n"
# Информация о чате
chat_title = message.chat.title if message.chat.title else "Личные сообщения"
report_text += f"💬 <b>Чат:</b> {chat_title}\n"
report_text += f"🆔 <b>Chat ID:</b> <code>{message.chat.id}</code>\n\n"
# Причина
report_text += f"📝 <b>Причина:</b> {reason}\n\n"
# Текст сообщения
report_text += f"📄 <b>Текст сообщения:</b>\n"
if reported_message.text:
truncated_text = truncate_text(reported_message.text, max_length=300)
report_text += f"<code>{truncated_text}</code>\n\n"
elif reported_message.caption:
truncated_caption = truncate_text(reported_message.caption, max_length=300)
report_text += f"<code>{truncated_caption}</code>\n\n"
else:
content_type = reported_message.content_type
report_text += f"<i>[{content_type}]</i>\n\n"
# Время
report_text += f"🕐 <b>Время:</b> {format_datetime(datetime.now())}\n"
report_text += f"🔗 <b>Message ID:</b> <code>{reported_message.message_id}</code>\n\n"
report_text += f"💡 <i>ID репорта: {report_id}</i>"
# Клавиатура
keyboard = get_report_keyboard(
chat_id=message.chat.id,
message_id=reported_message.message_id,
reported_user_id=reported_user.id,
report_id=report_id
)
# === ОТПРАВКА РЕПОРТА ===
try:
# Если указан админ-чат, отправляем туда
if REPORT_CHAT_ID:
await message.bot.send_message(
chat_id=REPORT_CHAT_ID,
text=report_text,
parse_mode="HTML",
reply_markup=keyboard.as_markup()
)
else:
# Отправляем всем владельцам
sent_count = 0
for owner_id in settings.OWNER_ID:
try:
await message.bot.send_message(
chat_id=owner_id,
text=report_text,
parse_mode="HTML",
reply_markup=keyboard.as_markup()
)
sent_count += 1
except Exception as e:
logger.error(f"Ошибка отправки репорта владельцу {owner_id}: {e}", log_type="REPORT")
if sent_count == 0:
raise Exception("Не удалось отправить репорт ни одному владельцу")
# Подтверждение пользователю
await message.answer(
"✅ <b>Жалоба отправлена администраторам</b>\n\n"
"Спасибо за бдительность! Администраторы рассмотрят вашу жалобу.",
parse_mode="HTML"
)
# Логирование
logger.info(
f"Репорт #{report_id}: {reporter.id}{reported_user.id} в чате {message.chat.id}",
log_type="REPORT"
)
except Exception as e:
logger.error(f"Ошибка отправки репорта: {e}", log_type="REPORT")
await message.answer(
"❌ <b>Ошибка отправки жалобы</b>\n\nПопробуйте позже или обратитесь к администратору напрямую.",
parse_mode="HTML"
)
# ================= ОБРАБОТЧИКИ КНОПОК =================
@router.callback_query(F.data.startswith("report:ban:"), IsAdmin())
async def report_ban_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Забанить'"""
try:
# Парсим данные: report:ban:chat_id:user_id:report_id
parts = callback.data.split(":")
chat_id = int(parts[2])
user_id = int(parts[3])
report_id = parts[4]
# Баним пользователя
try:
await callback.bot.ban_chat_member(
chat_id=chat_id,
user_id=user_id
)
admin_name = format_user(callback.from_user)
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
await callback.answer("✅ Пользователь забанен", show_alert=True)
logger.info(
f"Репорт #{report_id}: пользователь {user_id} забанен админом {callback.from_user.id}",
log_type="REPORT"
)
except TelegramBadRequest as e:
await callback.answer(f"❌ Ошибка бана: {str(e)}", show_alert=True)
except Exception as e:
logger.error(f"Ошибка обработки бана из репорта: {e}", log_type="REPORT")
await callback.answer("❌ Ошибка выполнения", show_alert=True)
@router.callback_query(F.data.startswith("report:delete:"), IsAdmin())
async def report_delete_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Удалить'"""
try:
# Парсим данные: report:delete:chat_id:message_id:report_id
parts = callback.data.split(":")
chat_id = int(parts[2])
message_id = int(parts[3])
report_id = parts[4]
# Удаляем сообщение
try:
await callback.bot.delete_message(
chat_id=chat_id,
message_id=message_id
)
admin_name = format_user(callback.from_user)
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
await callback.answer("✅ Сообщение удалено", show_alert=True)
logger.info(
f"Репорт #{report_id}: сообщение {message_id} удалено админом {callback.from_user.id}",
log_type="REPORT"
)
except TelegramBadRequest as e:
await callback.answer(f"❌ Ошибка удаления: {str(e)}", show_alert=True)
except Exception as e:
logger.error(f"Ошибка удаления из репорта: {e}", log_type="REPORT")
await callback.answer("❌ Ошибка выполнения", show_alert=True)
@router.callback_query(F.data.startswith("report:close:"), IsAdmin())
async def report_close_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Закрыть'"""
try:
# Парсим данные: report:close:report_id
parts = callback.data.split(":")
report_id = parts[2]
admin_name = format_user(callback.from_user)
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n✅ <b>Репорт закрыт</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
await callback.answer("✅ Репорт закрыт")
logger.info(
f"Репорт #{report_id} закрыт админом {callback.from_user.id}",
log_type="REPORT"
)
except Exception as e:
logger.error(f"Ошибка закрытия репорта: {e}", log_type="REPORT")
await callback.answer("❌ Ошибка выполнения", show_alert=True)
# ================= ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ =================
@router.message(Command(*COMMANDS.get("reporthelp", ["reporthelp"]), prefix=settings.PREFIX, ignore_case=True))
async def report_help_cmd(message: Message) -> None:
"""
Показывает справку по системе репортов.
Доступно всем пользователям.
"""
text = (
"🚨 <b>СИСТЕМА РЕПОРТОВ</b>\n\n"
"Используйте команду /report, чтобы пожаловаться на сообщение администраторам.\n\n"
"📝 <b>Как пожаловаться:</b>\n"
"1. Ответьте на сообщение нарушителя\n"
"2. Напишите <code>/report</code>\n"
"3. Можно указать причину: <code>/report спам</code>\n\n"
"✅ <b>Примеры:</b>\n"
"• <code>/report</code> — жалоба без причины\n"
"• <code>/report спам</code> — жалоба на спам\n"
"• <code>/report оскорбления</code> — жалоба на оскорбления\n\n"
"⚠️ <b>Важно:</b>\n"
"├─ Нельзя пожаловаться на себя\n"
"├─ Нельзя пожаловаться на ботов\n"
"├─ Нельзя пожаловаться на администраторов\n"
"└─ Ложные жалобы могут привести к бану\n\n"
"💡 <i>Администраторы получат уведомление и примут меры</i>"
)
await message.answer(text, parse_mode="HTML")
@router.message(Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
async def report_stats_cmd(message: Message) -> None:
"""
Показывает статистику по репортам (для админов).
TODO: Реализовать сохранение статистики в БД
"""
text = (
"📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
"⚠️ <i>Функция в разработке</i>\n\n"
"Планируется:\n"
"Всего репортов за всё время\n"
"• Топ жалобщиков\n"
"• Топ нарушителей\n"
"• Распределение по причинам\n"
"• Статистика обработки\n\n"
"💡 <i>Для реализации нужно добавить таблицу reports в БД</i>"
)
await message.answer(text, parse_mode="HTML")

View File

@@ -0,0 +1,346 @@
"""
Обработчики команд режима тишины
"""
from datetime import datetime
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from database import get_manager
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
router: Router = Router(name="silence_mode_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def parse_silence_args(text: str) -> tuple[bool, str | int]:
"""
Парсит аргументы команды для режима тишины.
Args:
text: Полный текст сообщения
Returns:
(success, result): result это либо минуты (int), либо текст ошибки (str)
"""
parts = text.split(maxsplit=1)
if len(parts) < 2:
return False, "❌ Использование: <code>/silence <минуты></code>"
return True, parts[1]
def format_time_str(minutes: int) -> str:
"""Форматирует время в читабельный формат"""
if minutes < 60:
return f"{minutes} мин"
elif minutes < 1440:
hours = minutes // 60
mins = minutes % 60
return f"{hours}ч {mins}м" if mins else f"{hours}ч"
else:
days = minutes // 1440
hours = (minutes % 1440) // 60
return f"{days}д {hours}ч" if hours else f"{days}д"
def format_datetime(dt: datetime) -> str:
"""Форматирует datetime в читабельный формат"""
return dt.strftime("%d.%m.%Y %H:%M:%S")
# ================= КОМАНДЫ РЕЖИМА ТИШИНЫ =================
@router.message(Command(*COMMANDS.get("silence", ["silence"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="START_SILENCE_MODE", log_args=True)
async def start_silence_mode_cmd(message: Message) -> None:
"""
Активирует режим тишины на указанное время.
В этом режиме удаляются ВСЕ сообщения от обычных пользователей.
Администраторы могут продолжать писать.
Использование: /silence <минуты>
Примеры:
/silence 30 — на 30 минут
/silence 120 — на 2 часа
/silence 1440 — на сутки
"""
success, result = parse_silence_args(message.text)
if not success:
await message.answer(result, parse_mode="HTML")
return
# Валидация минут
try:
minutes = int(result)
if minutes < 1 or minutes > 10080: # Максимум неделя
await message.answer(
"❌ Время должно быть от 1 минуты до 10080 минут (7 дней)",
parse_mode="HTML"
)
return
except ValueError:
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
return
manager = get_manager()
try:
# Проверяем, уже активен ли режим
is_already_active = await manager.is_silence_active()
# Активируем режим (перезаписывает предыдущий, если был)
expires_at = await manager.set_silence_mode(minutes)
time_str = format_time_str(minutes)
expires_str = format_datetime(expires_at)
if is_already_active:
action_text = "🔄 <b>РЕЖИМ ТИШИНЫ ОБНОВЛЁН</b>"
else:
action_text = "🔇 <b>РЕЖИМ ТИШИНЫ АКТИВИРОВАН</b>"
text = (
f"{action_text}\n\n"
f"⏱ Длительность: {time_str}\n"
f"🕐 Окончание: {expires_str}\n\n"
f"⚠️ <b>Что происходит:</b>\n"
f"├─ Все сообщения от пользователей удаляются\n"
f"├─ Администраторы могут писать\n"
f"└─ Банворды временно отключены\n\n"
f"💡 <i>Используйте для успокоения спора или флуда</i>\n"
f"Отключить досрочно: /unsilence"
)
await message.answer(text, parse_mode="HTML")
logger.info(
f"Режим тишины {'обновлён' if is_already_active else 'активирован'} на {minutes} мин "
f"пользователем {message.from_user.id}",
log_type="SILENCE"
)
except Exception as e:
logger.error(f"Ошибка активации режима тишины: {e}", log_type="SILENCE")
await message.answer("❌ <b>Ошибка активации режима</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("unsilence", ["unsilence"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="STOP_SILENCE_MODE")
async def stop_silence_mode_cmd(message: Message) -> None:
"""
Отключает режим тишины.
Использование: /unsilence
"""
manager = get_manager()
try:
# Проверяем, активен ли режим
is_active = await manager.is_silence_active()
if not is_active:
await message.answer(
"⚠️ <b>Режим тишины не активен</b>\n\n"
"Активируйте командой: /silence <минуты>",
parse_mode="HTML"
)
return
# Отключаем режим
await manager.disable_silence_mode()
text = (
f"✅ <b>Режим тишины отключен</b>\n\n"
f"🔊 Пользователи снова могут отправлять сообщения\n"
f"🔄 Банворды снова активны"
)
await message.answer(text, parse_mode="HTML")
logger.info(
f"Режим тишины отключён пользователем {message.from_user.id}",
log_type="SILENCE"
)
except Exception as e:
logger.error(f"Ошибка отключения режима тишины: {e}", log_type="SILENCE")
await message.answer("❌ <b>Ошибка отключения режима</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("silencestatus", ["silencestatus"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="SILENCE_STATUS")
async def silence_status_cmd(message: Message) -> None:
"""
Показывает статус режима тишины.
Использование: /silencestatus
"""
manager = get_manager()
try:
# Проверяем активность режима
is_active = await manager.is_silence_active()
if is_active:
# Режим активен - показываем детали
silence_until_str = await manager.repo.get_setting("silence_until")
silence_until = float(silence_until_str)
expires_at = datetime.fromtimestamp(silence_until)
now = datetime.now()
time_left_seconds = (expires_at - now).total_seconds()
time_left_minutes = int(time_left_seconds / 60)
# Расчёт процента прошедшего времени (для визуализации)
# Примерно определяем начальное время
started_minutes_ago = 0 # Можно было бы сохранять в БД
text = (
f"🔇 <b>РЕЖИМ ТИШИНЫ АКТИВЕН</b>\n\n"
f"⏱ Осталось: {format_time_str(time_left_minutes)}\n"
f"🕐 Окончание: {format_datetime(expires_at)}\n\n"
f"⚠️ <b>Что происходит:</b>\n"
f"├─ Все сообщения от пользователей удаляются\n"
f"├─ Администраторы могут писать\n"
f"└─ Банворды временно отключены\n\n"
f"💡 <i>Для успокоения конфликта или флуда</i>\n"
f"Отключить: /unsilence"
)
# Добавляем визуальную шкалу прогресса
if time_left_minutes <= 60:
progress_bar = create_progress_bar(time_left_minutes, 60)
text += f"\n\n{progress_bar}"
else:
# Режим не активен
text = (
f"💤 <b>Режим тишины НЕ активен</b>\n\n"
f"🔊 Пользователи могут отправлять сообщения\n"
f"🔄 Банворды работают в обычном режиме\n\n"
f"Активировать: /silence <минуты>"
)
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка получения статуса режима тишины: {e}", log_type="SILENCE")
await message.answer("❌ <b>Ошибка получения статуса</b>", parse_mode="HTML")
def create_progress_bar(minutes_left: int, total_minutes: int, length: int = 10) -> str:
"""
Создает визуальную шкалу прогресса.
Args:
minutes_left: Сколько минут осталось
total_minutes: Всего минут
length: Длина шкалы
Returns:
Строка с визуальной шкалой
"""
if total_minutes <= 0:
filled = 0
else:
filled = int((total_minutes - minutes_left) / total_minutes * length)
filled = max(0, min(filled, length))
empty = length - filled
bar = "" * filled + "" * empty
percentage = int((total_minutes - minutes_left) / total_minutes * 100) if total_minutes > 0 else 0
return f"[{bar}] {percentage}%"
@router.message(Command(*COMMANDS.get("extend_silence", ["extend_silence"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="EXTEND_SILENCE_MODE", log_args=True)
async def extend_silence_mode_cmd(message: Message) -> None:
"""
Продлевает режим тишины на указанное время.
Использование: /extend_silence <минуты>
Пример: /extend_silence 30
"""
success, result = parse_silence_args(message.text)
if not success:
# Меняем текст ошибки для extend команды
await message.answer(
"❌ Использование: <code>/extend_silence <минуты></code>",
parse_mode="HTML"
)
return
# Проверяем, активен ли режим
manager = get_manager()
is_active = await manager.is_silence_active()
if not is_active:
await message.answer(
"⚠️ <b>Режим тишины не активен</b>\n\n"
"Сначала активируйте: /silence <минуты>",
parse_mode="HTML"
)
return
try:
add_minutes = int(result)
if add_minutes < 1 or add_minutes > 1440:
await message.answer(
"❌ Время продления должно быть от 1 до 1440 минут (24 часа)",
parse_mode="HTML"
)
return
except ValueError:
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
return
try:
# Получаем текущее время окончания
silence_until_str = await manager.repo.get_setting("silence_until")
current_until = float(silence_until_str)
current_expires = datetime.fromtimestamp(current_until)
# Вычисляем сколько минут осталось + добавляем новые
now = datetime.now()
current_minutes_left = int((current_expires - now).total_seconds() / 60)
new_total_minutes = current_minutes_left + add_minutes
# Устанавливаем новое время
new_expires_at = await manager.set_silence_mode(new_total_minutes)
time_str = format_time_str(add_minutes)
new_expires_str = format_datetime(new_expires_at)
text = (
f"⏱ <b>РЕЖИМ ТИШИНЫ ПРОДЛЁН</b>\n\n"
f" Добавлено: {time_str}\n"
f"🕐 Новое окончание: {new_expires_str}\n"
f"Всего осталось: {format_time_str(new_total_minutes)}\n\n"
f"Отключить: /unsilence"
)
await message.answer(text, parse_mode="HTML")
logger.info(
f"Режим тишины продлён на {add_minutes} мин (всего: {new_total_minutes} мин)",
log_type="SILENCE"
)
except Exception as e:
logger.error(f"Ошибка продления режима тишины: {e}", log_type="SILENCE")
await message.answer("❌ <b>Ошибка продления режима</b>\n\nПопробуйте позже", parse_mode="HTML")

View File

@@ -0,0 +1,168 @@
"""
Обработчик команды /start и /help для администраторов.
Показывает список доступных команд для управления банвордами.
"""
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
CMD: str = "start"
router: Router = Router(name="start_cmd_router")
def kb(text: str = "Создатель⬆️", url: str = "https://t.me/verdise"):
ikb = InlineKeyboardBuilder()
ikb.button(text=text, url=url)
return ikb.as_markup()
@router.callback_query(F.data.casefold() == CMD)
@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="START_COMMAND", log_args=True)
async def start_cmd(update: Message | CallbackQuery) -> None:
"""
Обработчик команды /start и /help.
Показывает справку по командам бота для администраторов.
Доступно только администраторам (суперадмин или доп. админ из БД).
Args:
update: Message или CallbackQuery
"""
print(123)
# Определяем тип update и извлекаем данные
if isinstance(update, CallbackQuery):
message = update.message
user_id = update.from_user.id
is_callback = True
else:
message = update
user_id = update.from_user.id
is_callback = False
# Проверяем, является ли пользователь суперадмином
is_super_admin = user_id in settings.OWNER_ID
# Формируем текст помощи
help_text = (
"🤖 <b>PrimoGuard - Бот-модератор</b>\n\n"
"Автоматическое удаление сообщений с запрещёнными словами.\n"
"Поддержка подстрок, лемм, временных блокировок и режимов модерации.\n\n"
)
# === Команды просмотра ===
help_text += (
"📋 <b>Просмотр:</b>\n"
"/list — список всех правил и слов\n"
"/stats — статистика по удалениям\n"
"/id — получение айди пользователя\n"
"/chatid — получение айди чата\n\n"
)
# === Постоянные банворды ===
help_text += (
" <b>Добавить банворд (постоянно):</b>\n"
"/addword <code>слово</code> — подстрока (простой поиск)\n"
"/addlemma <code>слово</code> — лемма (все формы слова)\n"
"/addpart <code>комбинация</code> — часть (поиск без пробелов)\n\n"
)
# === Временные банворды ===
help_text += (
"⏱ <b>Добавить банворд (временно):</b>\n"
"/addtempword <code>слово минуты</code> — временная подстрока\n"
"/addtemplemma <code>слово минуты</code> — временная лемма\n"
"<i>Пример: /addtempword спам 60</i>\n\n"
)
# === Исключения (whitelist) ===
help_text += (
"✅ <b>Исключения (whitelist):</b>\n"
"/addexcept <code>текст</code> — добавить исключение\n"
"/remexcept <code>текст</code> — удалить исключение\n"
"<i>Исключения не проверяются фильтром</i>\n\n"
)
# === Режимы модерации ===
help_text += (
"🔇 <b>Режим тишины:</b>\n"
"/silence <code>минуты</code> — удалять ВСЕ сообщения\n"
"/unsilence — отключить режим тишины\n\n"
)
help_text += (
"⚔️ <b>Режим антиконфликта:</b>\n"
"/addconflictword <code>слово</code> — добавить конфликтное слово\n"
"/addconflictlemma <code>слово</code> — добавить конфликтную лемму\n"
"/stopconflict <code>минуты</code> — активировать режим\n"
"/unstopconflict — отключить режим\n\n"
)
# === Удаление ===
help_text += (
" <b>Удалить:</b>\n"
"/remword <code>слово</code> — удалить подстроку\n"
"/remlemma <code>слово</code> — удалить лемму\n"
"/rempart <code>комбинация</code> — удалить часть\n"
"/remtempword <code>слово</code> — удалить временную подстроку\n"
"/remtemplemma <code>слово</code> — удалить временную лемму\n"
"/remconflictword <code>слово</code> — удалить конфликтное слово\n"
"/remconflictlemma <code>слово</code> — удалить конфликтную лемму\n\n"
)
# === Управление админами (только для суперадминов) ===
if is_super_admin:
help_text += (
"👑 <b>Управление админами (только для владельцев):</b>\n"
"/addadmin <code>ID</code> — добавить администратора\n"
"/remadmin <code>ID</code> — удалить администратора\n"
"/listadmins — список всех админов\n\n"
)
# === Типы проверок ===
help_text += (
" <b>Типы проверок:</b>\n"
"• <b>Подстрока</b> — простой поиск в тексте\n"
"• <b>Лемма</b> — все формы слова (купить→куплю, купил, купишь...)\n"
"• <b>Часть</b> — поиск без пробелов (обходит \"к у п и т ь\")\n"
"• <b>Временные</b> — автоматически удаляются через N минут\n"
"• <b>Конфликтные</b> — работают только в режиме /stopconflict\n\n"
)
help_text += (
"🔧 <b>Технологии:</b>\n"
"• Unicode-нормализация (латиница→кириллица)\n"
"• Обход через разделители (\"с п а м\"\"спам\")\n"
"• Морфологический анализ (pymorphy3)\n"
"• SQLAlchemy + SQLite с кэшированием\n\n"
"💾 Все настройки сохраняются в базе данных"
)
# Отправляем ответ
try:
if is_callback:
await message.edit_text(
text=help_text,
parse_mode="HTML",
reply_markup=kb()
)
await update.answer()
else:
await message.answer(
text=help_text,
parse_mode="HTML",
reply_markup=kb()
)
except Exception as e:
logger.error(
f"Ошибка отправки help сообщения: {e}",
log_type="ERROR"
)
if is_callback:
await update.answer("❌ Ошибка отображения справки", show_alert=True)

View File

@@ -0,0 +1,589 @@
"""
Обработчики команды статистики
"""
from datetime import datetime
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from database import get_manager
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
router: Router = Router(name="stats_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def format_number(num: int) -> str:
"""Форматирует большие числа с разделителями"""
return f"{num:,}".replace(",", " ")
def create_text_bar(value: int, max_value: int, length: int = 10) -> str:
"""Создает текстовую полоску прогресса"""
if max_value == 0:
return "" * length
filled = int((value / max_value) * length)
filled = max(0, min(filled, length))
empty = length - filled
return "" * filled + "" * empty
def format_datetime(dt: datetime) -> str:
"""Форматирует datetime в читабельный формат"""
return dt.strftime("%d.%m.%Y %H:%M")
def format_time_remaining(minutes: int) -> str:
"""
Форматирует оставшееся время в читабельный формат.
Args:
minutes: Количество минут
Returns:
Отформатированная строка времени
"""
if minutes <= 0:
return "истёк"
elif minutes < 60:
return f"{minutes} мин"
elif minutes < 1440: # < 24 часов
hours = minutes // 60
mins = minutes % 60
if mins > 0:
return f"{hours}ч {mins}м"
return f"{hours}ч"
else: # >= 24 часов
days = minutes // 1440
hours = (minutes % 1440) // 60
if hours > 0:
return f"{days}д {hours}ч"
return f"{days}д"
def get_stats_keyboard():
"""Клавиатура для статистики"""
ikb = InlineKeyboardBuilder()
ikb.button(text="🔄 Обновить", callback_data="stats:refresh")
ikb.button(text="📊 Детали", callback_data="stats:details")
ikb.button(text="🏆 Топ-спамеры", callback_data="stats:top_spammers")
ikb.button(text="🔤 Топ-слова", callback_data="stats:top_words")
ikb.button(text="🚀 Назад", callback_data="start")
ikb.adjust(2, 2, 1)
return ikb.as_markup()
# ================= ОСНОВНАЯ СТАТИСТИКА =================
@router.callback_query(F.data == "stats:refresh")
@router.callback_query(F.data == "stats")
@router.message(Command(*COMMANDS.get("stats", ["stats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="VIEW_STATS")
async def stats_cmd(update: Message | CallbackQuery) -> None:
"""
Показывает общую статистику работы бота.
Включает:
- Общее количество удалений
- Активные режимы
- Статистику банвордов
- Топ спамеров
Использование: /stats
"""
# Определяем тип update
if isinstance(update, CallbackQuery):
message = update.message
is_callback = True
else:
message = update
is_callback = False
manager = get_manager()
try:
# Получаем данные
stats = await manager.get_stats()
data = await manager.get_all_words_list()
top_spammers = await manager.get_top_spammers(limit=5)
# Проверяем активные режимы
is_silence = await manager.is_silence_active()
is_conflict = await manager.is_conflict_active()
# === ФОРМИРУЕМ ВЫВОД ===
output = "📊 <b>СТАТИСТИКА PRIMOGUARD</b>\n\n"
# Общая информация
total_deletions = stats.get('total_deletions', 0)
output += f"🗑 <b>Всего удалений:</b> <code>{format_number(total_deletions)}</code>\n\n"
# Активные режимы
if is_silence or is_conflict:
output += "🔴 <b>АКТИВНЫЕ РЕЖИМЫ:</b>\n\n"
if is_silence:
silence_until_str = await manager.repo.get_setting("silence_until")
silence_until = datetime.fromtimestamp(float(silence_until_str))
time_left_seconds = (silence_until - datetime.now()).total_seconds()
time_left_minutes = int(time_left_seconds / 60)
output += f"🔇 <b>Режим тишины</b>\n"
output += f"├─ ⏱ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
output += f"└─ 🕐 До: {format_datetime(silence_until)}\n"
if is_conflict:
output += "\n"
if is_conflict:
conflict_until_str = await manager.repo.get_setting("conflict_until")
conflict_until = datetime.fromtimestamp(float(conflict_until_str))
time_left_seconds = (conflict_until - datetime.now()).total_seconds()
time_left_minutes = int(time_left_seconds / 60)
conflict_words_count = len(data.get('conflict_substring', set()))
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
total_conflict = conflict_words_count + conflict_lemmas_count
output += f"⚔️ <b>Режим антиконфликта</b>\n"
output += f"├─ ⏱ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
output += f"├─ 🕐 До: {format_datetime(conflict_until)}\n"
output += f"└─ 📊 Правил: <code>{total_conflict}</code>\n"
output += "\n"
# Статистика правил
total_rules = (
len(data.get('substring', set())) +
len(data.get('lemma', set())) +
len(data.get('part', set())) +
len(data.get('temp_substring', set())) +
len(data.get('temp_lemma', set())) +
len(data.get('conflict_substring', set())) +
len(data.get('conflict_lemma', set()))
)
output += f"📋 <b>Правила модерации:</b>\n"
output += f"├─ Всего правил: <code>{total_rules}</code>\n"
output += f"├─ Постоянные: <code>{len(data.get('substring', set())) + len(data.get('lemma', set())) + len(data.get('part', set()))}</code>\n"
output += f"├─ Временные: <code>{len(data.get('temp_substring', set())) + len(data.get('temp_lemma', set()))}</code>\n"
output += f"├─ Конфликтные: <code>{len(data.get('conflict_substring', set())) + len(data.get('conflict_lemma', set()))}</code>\n"
output += f"└─ Исключения: <code>{len(data.get('whitelist', set()))}</code>\n\n"
# Топ-5 спамеров
if top_spammers:
output += "🏆 <b>Топ-5 спамеров:</b>\n"
max_count = top_spammers[0][1] if top_spammers else 1
for idx, (user_id, count) in enumerate(top_spammers, 1):
bar = create_text_bar(count, max_count, length=8)
output += f"{idx}. <code>{user_id}</code> — {count} [{bar}]\n"
output += "\n"
else:
output += "🏆 <b>Топ-5 спамеров:</b>\n"
output += "└─ <i>Нет данных</i>\n\n"
# Администраторы
admins_count = len(settings.OWNER_ID) + len(data.get('admins', set()))
output += f"👥 <b>Администраторов:</b> <code>{admins_count}</code>\n\n"
# Подсказка
output += "💡 <i>Используйте кнопки для детальной информации</i>"
# Клавиатура
keyboard = get_stats_keyboard()
# Отправка
if is_callback:
await message.edit_text(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)
await update.answer("✅ Статистика обновлена")
else:
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка получения статистики: {e}", log_type="STATS")
error_text = "❌ <b>Ошибка загрузки статистики</b>\n\nПопробуйте позже"
if is_callback:
await update.answer("❌ Ошибка загрузки", show_alert=True)
else:
await message.answer(error_text, parse_mode="HTML")
# ================= ДЕТАЛЬНАЯ СТАТИСТИКА =================
@router.callback_query(F.data == "stats:details")
@log_action(action_name="VIEW_DETAILED_STATS")
async def stats_details_callback(callback: CallbackQuery) -> None:
"""Показывает детальную статистику"""
manager = get_manager()
try:
stats = await manager.get_stats()
data = await manager.get_all_words_list()
output = "📊 <b>ДЕТАЛЬНАЯ СТАТИСТИКА</b>\n\n"
# Подробная статистика удалений
total_deletions = stats.get('total_deletions', 0)
output += f"🗑 <b>Удаления сообщений:</b>\n"
output += f"├─ Всего: <code>{format_number(total_deletions)}</code>\n"
output += "\n"
# Активные режимы (детально)
is_silence = await manager.is_silence_active()
is_conflict = await manager.is_conflict_active()
if is_silence or is_conflict:
output += "🔴 <b>Активные режимы:</b>\n\n"
if is_silence:
silence_until_str = await manager.repo.get_setting("silence_until")
silence_until = datetime.fromtimestamp(float(silence_until_str))
time_left_seconds = (silence_until - datetime.now()).total_seconds()
time_left_minutes = int(time_left_seconds / 60)
output += f"🔇 <b>Режим тишины:</b>\n"
output += f"├─ Статус: ✅ Активен\n"
output += f"├─ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
output += f"├─ Окончание: {format_datetime(silence_until)}\n"
output += f"└─ Эффект: Удаляются ВСЕ сообщения\n\n"
if is_conflict:
conflict_until_str = await manager.repo.get_setting("conflict_until")
conflict_until = datetime.fromtimestamp(float(conflict_until_str))
time_left_seconds = (conflict_until - datetime.now()).total_seconds()
time_left_minutes = int(time_left_seconds / 60)
conflict_words_count = len(data.get('conflict_substring', set()))
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
output += f"⚔️ <b>Режим антиконфликта:</b>\n"
output += f"├─ Статус: ✅ Активен\n"
output += f"├─ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
output += f"├─ Окончание: {format_datetime(conflict_until)}\n"
output += f"├─ Слов: <code>{conflict_words_count}</code>\n"
output += f"├─ Лемм: <code>{conflict_lemmas_count}</code>\n"
output += f"└─ Эффект: Обычные банворды отключены\n\n"
# Детальная статистика правил
output += f"📋 <b>Правила модерации:</b>\n\n"
output += f"🔴 <b>Постоянные:</b>\n"
output += f"├─ Подстроки: <code>{len(data.get('substring', set()))}</code>\n"
output += f"├─ Леммы: <code>{len(data.get('lemma', set()))}</code>\n"
output += f"└─ Части: <code>{len(data.get('part', set()))}</code>\n\n"
output += f"⏱ <b>Временные:</b>\n"
output += f"├─ Подстроки: <code>{len(data.get('temp_substring', set()))}</code>\n"
output += f"└─ Леммы: <code>{len(data.get('temp_lemma', set()))}</code>\n\n"
output += f"⚔️ <b>Конфликтные:</b>\n"
output += f"├─ Слова: <code>{len(data.get('conflict_substring', set()))}</code>\n"
output += f"└─ Леммы: <code>{len(data.get('conflict_lemma', set()))}</code>\n\n"
output += f"✅ <b>Исключения:</b> <code>{len(data.get('whitelist', set()))}</code>\n\n"
# Информация о кэше
cache_info = stats.get('cache_active', False)
cache_updated = stats.get('cache_updated_at', None)
output += f"💾 <b>Кэш:</b>\n"
output += f"├─ Статус: {'✅ Активен' if cache_info else '❌ Неактивен'}\n"
if cache_updated and isinstance(cache_updated, str):
try:
updated_dt = datetime.fromisoformat(cache_updated)
output += f"└─ Обновлён: {format_datetime(updated_dt)}\n"
except (ValueError, TypeError):
output += f"└─ Обновлён: недавно\n"
else:
output += f"└─ Не обновлялся\n"
# Кнопка возврата
ikb = InlineKeyboardBuilder()
ikb.button(text="◀️ Назад", callback_data="stats:refresh")
await callback.message.edit_text(
text=output,
parse_mode="HTML",
reply_markup=ikb.as_markup()
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка получения детальной статистики: {e}", log_type="STATS")
await callback.answer("❌ Ошибка загрузки", show_alert=True)
# ================= ТОП СПАМЕРОВ =================
@router.callback_query(F.data == "stats:top_spammers")
@log_action(action_name="VIEW_TOP_SPAMMERS")
async def stats_top_spammers_callback(callback: CallbackQuery) -> None:
"""Показывает топ-10 спамеров"""
manager = get_manager()
try:
top_spammers = await manager.get_top_spammers(limit=10)
output = "🏆 <b>ТОП-10 СПАМЕРОВ</b>\n\n"
if top_spammers:
max_count = top_spammers[0][1] if top_spammers else 1
for idx, (user_id, count) in enumerate(top_spammers, 1):
bar = create_text_bar(count, max_count, length=10)
# Эмодзи для топ-3
if idx == 1:
medal = "🥇"
elif idx == 2:
medal = "🥈"
elif idx == 3:
medal = "🥉"
else:
medal = f"{idx}."
output += f"{medal} <code>{user_id}</code>\n"
output += f" └─ {format_number(count)} удалений [{bar}]\n\n"
# Общая статистика
total_spammers = len(top_spammers)
total_deletions = sum(count for _, count in top_spammers)
output += f"📊 <b>Статистика:</b>\n"
output += f"├─ Всего пользователей: <code>{total_spammers}</code>\n"
output += f"└─ Всего удалений: <code>{format_number(total_deletions)}</code>\n\n"
output += "💡 <i>ID можно использовать для проверки пользователя</i>"
else:
output += "└─ <i>Нет данных об удалениях</i>\n\n"
output += "💡 <i>Когда бот начнёт удалять сообщения, здесь появится статистика</i>"
# Кнопка возврата
ikb = InlineKeyboardBuilder()
ikb.button(text="◀️ Назад", callback_data="stats:refresh")
await callback.message.edit_text(
text=output,
parse_mode="HTML",
reply_markup=ikb.as_markup()
)
await callback.answer()
except Exception as e:
logger.error(f"Ошибка получения топ спамеров: {e}", log_type="STATS")
await callback.answer("❌ Ошибка загрузки", show_alert=True)
# ================= ТОП СЛОВ =================
@router.callback_query(F.data == "stats_top_words")
async def stats_top_words_callback(callback: CallbackQuery) -> None:
"""Показывает топ-10 самых частых срабатываний"""
await callback.answer()
manager = get_manager()
# Получаем топ слов
top_words = await manager.get_top_words(limit=10)
if not top_words:
text = (
"🔤 <b>ТОП-10 СРАБАТЫВАНИЙ ПО СЛОВАМ</b>\n\n"
"📭 <i>Статистика пока пуста</i>\n\n"
"Срабатывания появятся после удаления\n"
"первых спам-сообщений."
)
else:
text = "🔤 <b>ТОП-10 СРАБАТЫВАНИЙ ПО СЛОВАМ</b>\n\n"
# Эмодзи для типов
type_emoji = {
"substring": "🔤",
"lemma": "📖",
"part": "🧩",
"silence": "🔇",
"conflict_substring": "⚔️",
"conflict_lemma": "⚔️"
}
for i, word_data in enumerate(top_words, 1):
word = word_data['word']
count = word_data['count']
word_type = word_data['type']
emoji = type_emoji.get(word_type, "")
# Медали для топ-3
medal = ""
if i == 1:
medal = "🥇 "
elif i == 2:
medal = "🥈 "
elif i == 3:
medal = "🥉 "
text += f"{medal}<b>{i}.</b> {emoji} <code>{word}</code> — {count} раз\n"
# Общая статистика
total = await manager.get_total_spam_count()
text += f"\n📊 <b>Всего удалено:</b> {total} сообщений"
# Кнопка назад
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="◀️ Назад", callback_data="show_stats")]
])
try:
await callback.message.edit_text(
text=text,
reply_markup=keyboard,
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка показа топ-слов: {e}", log_type="ERROR")
await callback.answer("❌ Ошибка загрузки статистики", show_alert=True)
# ================= СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ =================
@router.message(Command(*COMMANDS.get("userstats", ["userstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="VIEW_USER_STATS", log_args=True)
async def user_stats_cmd(message: Message) -> None:
"""
Показывает статистику конкретного пользователя.
Использование: /userstats <ID>
Пример: /userstats 123456789
"""
parts = message.text.split(maxsplit=1)
if len(parts) < 2:
await message.answer(
"❌ Использование: <code>/userstats [ID]</code>\n\n"
"Пример: <code>/userstats 123456789</code>",
parse_mode="HTML"
)
return
try:
user_id = int(parts[1].strip())
except ValueError:
await message.answer("❌ ID должен быть числом", parse_mode="HTML")
return
manager = get_manager()
try:
# Получаем статистику пользователя
user_spam_count = await manager.get_user_spam_count(user_id)
user_spam_stats = await manager.get_spam_stats(limit=10, user_id=user_id)
output = f"👤 <b>СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ</b>\n\n"
output += f"🆔 ID: <code>{user_id}</code>\n\n"
if user_spam_count > 0:
output += f"🗑 <b>Удалено сообщений:</b> <code>{format_number(user_spam_count)}</code>\n\n"
if user_spam_stats:
output += f"📝 <b>Последние удаления:</b>\n"
for stat in user_spam_stats[:5]:
deleted_at = stat.deleted_at
matched_word = stat.matched_word or "неизвестно"
match_type = stat.match_type or "unknown"
output += f"├─ {format_datetime(deleted_at)}\n"
output += f"│ └─ Слово: <code>{matched_word}</code> ({match_type})\n"
output += "\n"
else:
output += "✅ <i>Нет нарушений</i>\n\n"
output += "Этот пользователь не нарушал правила чата"
await message.answer(output, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка получения статистики пользователя: {e}", log_type="STATS")
await message.answer("❌ <b>Ошибка загрузки статистики</b>", parse_mode="HTML")
# ================= СБРОС СТАТИСТИКИ =================
@router.message(Command(*COMMANDS.get("resetstats", ["resetstats"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="RESET_STATS")
async def reset_stats_cmd(message: Message) -> None:
"""
Сбрасывает всю статистику удалений.
⚠️ ВНИМАНИЕ: Это действие необратимо!
Использование: /resetstats confirm
"""
parts = message.text.split(maxsplit=1)
if len(parts) < 2 or parts[1].lower() != "confirm":
await message.answer(
"⚠️ <b>ВНИМАНИЕ!</b>\n\n"
"Эта команда удалит ВСЮ статистику удалений:\n"
"• Счётчики удалений пользователей\n"
"• Историю удалённых сообщений\n"
"• Топ спамеров\n\n"
"Правила модерации НЕ будут удалены.\n\n"
"Для подтверждения используйте:\n"
"<code>/resetstats confirm</code>",
parse_mode="HTML"
)
return
manager = get_manager()
try:
# Сбрасываем статистику
deleted_count = await manager.reset_spam_stats()
if deleted_count > 0:
await message.answer(
f"✅ <b>Статистика сброшена</b>\n\n"
f"Удалено записей: {deleted_count}\n\n"
f"Новые данные начнут собираться\n"
f"с этого момента.",
parse_mode="HTML"
)
logger.warning(
f"Статистика сброшена пользователем {message.from_user.id}: "
f"удалено {deleted_count} записей",
log_type="STATS"
)
else:
await message.answer(
" <b>Статистика уже пуста</b>",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка сброса статистики: {e}", log_type="STATS")
await message.answer("❌ <b>Ошибка сброса статистики</b>", parse_mode="HTML")

View File

@@ -0,0 +1,546 @@
"""
Обработчики команд добавления и удаления банвордов
"""
from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
from database import get_manager
from database.models import BanWordType
from middleware.loggers import logger
from bot.utils.decorators import log_action
__all__ = ("router",)
router: Router = Router(name="manage_words_router")
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def parse_args(text: str, command: str, min_args: int = 1, max_args: int = 2) -> tuple[bool, str | list]:
"""
Парсит аргументы команды.
Args:
text: Полный текст сообщения
command: Название команды
min_args: Минимальное количество аргументов
max_args: Максимальное количество аргументов
Returns:
(success, result): result это либо список аргументов, либо текст ошибки
"""
# Убираем команду из текста
parts = text.split(maxsplit=max_args)
if len(parts) < min_args + 1:
return False, f"❌ Использование: <code>/{command} {'<слово>' if min_args == 1 else '<слово> <минуты>'}</code>"
args = parts[1:]
# Валидация длины слова
if args and len(args[0]) < 2:
return False, "❌ Слово должно содержать минимум 2 символа"
if args and len(args[0]) > 100:
return False, "❌ Слово слишком длинное (максимум 100 символов)"
return True, args
def format_success_message(action: str, word: str, word_type: str, extra: str = "") -> str:
"""Форматирует сообщение об успехе"""
emoji_map = {
'добавлена': '',
'добавлен': '',
'добавлено': '',
'удалена': '🗑',
'удален': '🗑',
'удалено': '🗑'
}
emoji = emoji_map.get(action, '')
message = f"{emoji} <b>{word_type.capitalize()}</b> <code>{word}</code> {action}"
if extra:
message += f"\n{extra}"
return message
# ================= КОМАНДЫ ДОБАВЛЕНИЯ =================
@router.message(Command(*COMMANDS.get("addword", ["addword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="ADD_WORD", log_args=True)
async def add_word_cmd(message: Message) -> None:
"""
Добавляет банворд-подстроку (постоянно).
Использование: /addword <слово>
"""
success, result = parse_args(message.text, "addword", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
added = await manager.add_banword(
word=word,
word_type=BanWordType.SUBSTRING,
added_by=message.from_user.id,
reason=f"Добавлено через команду"
)
if added:
text = format_success_message(
"добавлена",
word,
"подстрока",
"🔍 Тип проверки: простой поиск в тексте"
)
else:
text = f"⚠️ Подстрока <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления банворда: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("addlemma", ["addlemma"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="ADD_LEMMA", log_args=True)
async def add_lemma_cmd(message: Message) -> None:
"""
Добавляет банворд-лемму (постоянно).
Использование: /addlemma <слово>
"""
success, result = parse_args(message.text, "addlemma", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
added = await manager.add_banword(
word=word,
word_type=BanWordType.LEMMA,
added_by=message.from_user.id,
reason=f"Добавлено через команду"
)
if added:
text = format_success_message(
"добавлена",
word,
"лемма",
"🔤 Тип проверки: все формы слова (купить→куплю, купил, купишь...)"
)
else:
text = f"⚠️ Лемма <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления леммы: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("addpart", ["addpart"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="ADD_PART", log_args=True)
async def add_part_cmd(message: Message) -> None:
"""
Добавляет банворд-часть (постоянно).
Использование: /addpart <комбинация>
"""
success, result = parse_args(message.text, "addpart", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
added = await manager.add_banword(
word=word,
word_type=BanWordType.PART,
added_by=message.from_user.id,
reason=f"Добавлено через команду"
)
if added:
text = format_success_message(
"добавлена",
word,
"часть",
"🧩 Тип проверки: поиск без пробелов (обходит \"к у п и т ь\")"
)
else:
text = f"⚠️ Часть <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления части: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("addtempword", ["addtempword"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="ADD_TEMP_WORD", log_args=True)
async def add_temp_word_cmd(message: Message) -> None:
"""
Добавляет временную банворд-подстроку.
Использование: /addtempword <слово> <минуты>
"""
success, result = parse_args(message.text, "addtempword", min_args=2, max_args=2)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
# Валидация минут
try:
minutes = int(result[1])
if minutes < 1 or minutes > 10080: # Максимум неделя
await message.answer("❌ Время должно быть от 1 минуты до 10080 минут (7 дней)", parse_mode="HTML")
return
except ValueError:
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
return
manager = get_manager()
try:
added = await manager.add_temp_banword(
word=word,
word_type=BanWordType.SUBSTRING,
minutes=minutes,
added_by=message.from_user.id
)
if added:
# Форматируем время
if minutes < 60:
time_str = f"{minutes} мин"
elif minutes < 1440:
hours = minutes // 60
mins = minutes % 60
time_str = f"{hours}ч {mins}м" if mins else f"{hours}ч"
else:
days = minutes // 1440
hours = (minutes % 1440) // 60
time_str = f"{days}д {hours}ч" if hours else f"{days}д"
text = format_success_message(
"добавлена",
word,
"временная подстрока",
f"⏱ Автоматически удалится через {time_str}"
)
else:
text = f"⚠️ Временная подстрока <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления временного банворда: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("addtemplemma", ["addtemplemma"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="ADD_TEMP_LEMMA", log_args=True)
async def add_temp_lemma_cmd(message: Message) -> None:
"""
Добавляет временную банворд-лемму.
Использование: /addtemplemma <слово> <минуты>
"""
success, result = parse_args(message.text, "addtemplemma", min_args=2, max_args=2)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
try:
minutes = int(result[1])
if minutes < 1 or minutes > 10080:
await message.answer("❌ Время должно быть от 1 минуты до 10080 минут (7 дней)", parse_mode="HTML")
return
except ValueError:
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
return
manager = get_manager()
try:
added = await manager.add_temp_banword(
word=word,
word_type=BanWordType.LEMMA,
minutes=minutes,
added_by=message.from_user.id
)
if added:
if minutes < 60:
time_str = f"{minutes} мин"
elif minutes < 1440:
hours = minutes // 60
mins = minutes % 60
time_str = f"{hours}ч {mins}м" if mins else f"{hours}ч"
else:
days = minutes // 1440
hours = (minutes % 1440) // 60
time_str = f"{days}д {hours}ч" if hours else f"{days}д"
text = format_success_message(
"добавлена",
word,
"временная лемма",
f"⏱ Автоматически удалится через {time_str}"
)
else:
text = f"⚠️ Временная лемма <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления временной леммы: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("addexcept", ["addexcept"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="ADD_EXCEPTION", log_args=True)
async def add_exception_cmd(message: Message) -> None:
"""
Добавляет исключение в whitelist.
Использование: /addexcept <текст>
"""
success, result = parse_args(message.text, "addexcept", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
added = await manager.add_whitelist(
word=word,
added_by=message.from_user.id,
reason="Добавлено через команду"
)
if added:
text = format_success_message(
"добавлено",
word,
"исключение",
"✅ Сообщения с этим текстом не будут проверяться"
)
else:
text = f"⚠️ Исключение <code>{word}</code> уже существует"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка добавления исключения: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
# ================= КОМАНДЫ УДАЛЕНИЯ =================
@router.message(Command(*COMMANDS.get("remword", ["remword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="REMOVE_WORD", log_args=True)
async def remove_word_cmd(message: Message) -> None:
"""
Удаляет банворд-подстроку.
Использование: /remword <слово>
"""
success, result = parse_args(message.text, "remword", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_banword(word=word, word_type=BanWordType.SUBSTRING)
if removed:
text = format_success_message("удалена", word, "подстрока")
else:
text = f"⚠️ Подстрока <code>{word}</code> не найдена"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления банворда: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("remlemma", ["remlemma"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="REMOVE_LEMMA", log_args=True)
async def remove_lemma_cmd(message: Message) -> None:
"""Удаляет банворд-лемму"""
success, result = parse_args(message.text, "remlemma", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_banword(word=word, word_type=BanWordType.LEMMA)
if removed:
text = format_success_message("удалена", word, "лемма")
else:
text = f"⚠️ Лемма <code>{word}</code> не найдена"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления леммы: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("rempart", ["rempart"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="REMOVE_PART", log_args=True)
async def remove_part_cmd(message: Message) -> None:
"""Удаляет банворд-часть"""
success, result = parse_args(message.text, "rempart", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_banword(word=word, word_type=BanWordType.PART)
if removed:
text = format_success_message("удалена", word, "часть")
else:
text = f"⚠️ Часть <code>{word}</code> не найдена"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления части: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("remtempword", ["remtempword"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="REMOVE_TEMP_WORD", log_args=True)
async def remove_temp_word_cmd(message: Message) -> None:
"""Удаляет временную подстроку"""
success, result = parse_args(message.text, "remtempword", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_temp_banword(word=word, word_type=BanWordType.SUBSTRING)
if removed:
text = format_success_message("удалена", word, "временная подстрока")
else:
text = f"⚠️ Временная подстрока <code>{word}</code> не найдена"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления временного банворда: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("remtemplemma", ["remtemplemma"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin())
@log_action(action_name="REMOVE_TEMP_LEMMA", log_args=True)
async def remove_temp_lemma_cmd(message: Message) -> None:
"""Удаляет временную лемму"""
success, result = parse_args(message.text, "remtemplemma", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_temp_banword(word=word, word_type=BanWordType.LEMMA)
if removed:
text = format_success_message("удалена", word, "временная лемма")
else:
text = f"⚠️ Временная лемма <code>{word}</code> не найдена"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления временной леммы: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
@router.message(Command(*COMMANDS.get("remexcept", ["remexcept"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@log_action(action_name="REMOVE_EXCEPTION", log_args=True)
async def remove_exception_cmd(message: Message) -> None:
"""Удаляет исключение из whitelist"""
success, result = parse_args(message.text, "remexcept", min_args=1, max_args=1)
if not success:
await message.answer(result, parse_mode="HTML")
return
word = result[0].lower().strip()
manager = get_manager()
try:
removed = await manager.remove_whitelist(word=word)
if removed:
text = format_success_message("удалено", word, "исключение")
else:
text = f"⚠️ Исключение <code>{word}</code> не найдено"
await message.answer(text, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка удаления исключения: {e}", log_type="CMD")
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")

View 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)

View 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

View 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