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

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

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