This commit is contained in:
2026-02-20 03:12:47 +07:00
parent 5d350d0885
commit 5aca4e8438
23 changed files with 2291 additions and 1330 deletions

View File

@@ -11,6 +11,8 @@ 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
from .cancel import router as cancel_router
from .bot_settings import router as setting_router
# Настройка экспорта и роутера
__all__ = ("router",)
@@ -19,6 +21,7 @@ router: Router = Router(name=__name__)
# Подключение роутеров
router.include_routers(
cancel_router,
notifications_router,
report_router,
admin_router,
@@ -30,4 +33,5 @@ conflict_router,
stats_router,
id_router,
emoji_router,
setting_router,
)

View File

@@ -6,248 +6,197 @@ from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot # ← ДОБАВЬ ЭТОТ ИМПОРТ
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
from bot.utils import log_action, tg_emoji
__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>"
return False, f'{tg_emoji("4961187972822074653")} Использование: <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 False, f'{tg_emoji("4961187972822074653")} ID должен быть положительным числом'
if user_id > 9999999999:
return False, f'{tg_emoji("4961187972822074653")} Некорректный ID пользователя'
return True, user_id
except ValueError:
return False, " ID должен быть числом"
return False, f'{tg_emoji("4961187972822074653")} ID должен быть числом'
async def get_user_display_name(user_id: int) -> str:
"""Получает имя пользователя или username или ID"""
try:
chat = await bot.get_chat(user_id)
name = f"{chat.first_name or ''} {chat.last_name or ''}".strip()
if name:
return name
if chat.username:
return f"@{chat.username}"
return str(user_id)
except:
return str(user_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>"
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.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),
@router.message(Command(*COMMANDS.get('addadmin', ['addadmin']), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="ADD_ADMIN", log_args=True)
@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")
success, result = parse_user_id(message.text, 'addadmin')
if not success:
await message.answer(result, parse_mode="HTML")
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"
f'{tg_emoji("4963024861615096794")} <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"
f'{tg_emoji("4963024861615096794")} <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:
display_name = await get_user_display_name(user_id)
await message.answer(
f"⚠️ Пользователь {format_admin_info(user_id)} уже является администратором",
parse_mode="HTML"
f'{tg_emoji("4963024861615096794")} Пользователь <b>{display_name}</b> уже является администратором',
parse_mode='HTML'
)
return
# Добавляем администратора
added = await manager.add_admin(
user_id=user_id,
added_by=message.from_user.id
)
added = await manager.add_admin(user_id=user_id, added_by=message.from_user.id)
if added:
display_name = await get_user_display_name(user_id)
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"
f'{tg_emoji("4963010134172239128")} <b>Администратор добавлен</b>\n\n'
f'{tg_emoji("4961064956368782417")} ID: {format_admin_info(user_id)}\n'
f'{tg_emoji("4963343509533754468")} Имя: <b>{display_name}</b>\n'
f'{tg_emoji("4963343509533754468")} Добавил: {format_admin_info(message.from_user.id, message.from_user.username)}\n\n'
f'{tg_emoji("4961106084975608869")} Права администратора:\n'
f'├─ Управление банвордами\n'
f'├─ Просмотр статистики\n'
f'├─ Активация режимов модерации\n'
f'└─ Все команды бота\n\n'
f'{tg_emoji("4963024861615096794")} <i>Не может управлять другими админами</i>\n'
f'Список админов: <b>/listadmins</b>'
)
logger.info(f'Администратор добавлен: {user_id} (добавил: {message.from_user.id})', log_type='ADMIN_MGMT')
else:
text = " <b>Ошибка добавления администратора</b>\n\nПопробуйте позже"
text = f'{tg_emoji("4961187972822074653")} <b>Ошибка добавления администратора</b>\n\nПопробуйте позже'
await message.answer(text, parse_mode="HTML")
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")
logger.error(f'Ошибка добавления администратора: {e}', log_type='ADMIN_MGMT')
await message.answer(f'{tg_emoji("4961187972822074653")} <b>Ошибка добавления</b>\n\nПопробуйте позже', parse_mode='HTML')
# ================= УДАЛЕНИЕ АДМИНИСТРАТОРА =================
@router.message(Command(*COMMANDS.get("remadmin", ["remadmin"]), prefix=settings.PREFIX, ignore_case=True),
@router.message(Command(*COMMANDS.get('remadmin', ['remadmin']), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="REMOVE_ADMIN", log_args=True)
@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")
success, result = parse_user_id(message.text, 'remadmin')
if not success:
await message.answer(result, parse_mode="HTML")
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"
f'{tg_emoji("4963024861615096794")} <b>Нельзя удалить владельца</b>\n\n'
'Владельцы имеют права постоянно',
parse_mode='HTML'
)
return
# Проверка: нельзя удалить самого себя (если вы владелец)
if user_id == message.from_user.id:
await message.answer(
"⚠️ <b>Нельзя удалить самого себя</b>",
parse_mode="HTML"
f'{tg_emoji("4963024861615096794")} <b>Нельзя удалить самого себя</b>',
parse_mode='HTML'
)
return
manager = get_manager()
try:
# Проверяем, является ли администратором
is_admin = await manager.is_admin(user_id)
if not is_admin:
display_name = await get_user_display_name(user_id)
await message.answer(
f"⚠️ Пользователь {format_admin_info(user_id)} не является администратором",
parse_mode="HTML"
f'{tg_emoji("4963024861615096794")} Пользователь <b>{display_name}</b> не является администратором',
parse_mode='HTML'
)
return
# Удаляем администратора
removed = await manager.remove_admin(user_id=user_id)
if removed:
display_name = await get_user_display_name(user_id)
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"
f'🗑 <b>Администратор удалён</b>\n\n'
f'{tg_emoji("4961064956368782417")} ID: {format_admin_info(user_id)}\n'
f'{tg_emoji("4961064956368782417")} Имя: <b>{display_name}</b>\n'
f'{tg_emoji("4963343509533754468")} Удалил: {format_admin_info(message.from_user.id, message.from_user.username)}\n\n'
f'{tg_emoji("4963024861615096794")} <i>Пользователь больше не имеет доступа к командам бота</i>'
)
logger.info(f'Администратор удалён: {user_id} (удалил: {message.from_user.id})', log_type='ADMIN_MGMT')
else:
text = " <b>Ошибка удаления администратора</b>\n\nПопробуйте позже"
text = f'{tg_emoji("4961187972822074653")} <b>Ошибка удаления администратора</b>\n\nПопробуйте позже'
await message.answer(text, parse_mode="HTML")
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")
logger.error(f'Ошибка удаления администратора: {e}', log_type='ADMIN_MGMT')
await message.answer(f'{tg_emoji("4961187972822074653")} <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),
@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")
@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
@@ -256,179 +205,143 @@ async def list_admins_cmd(update: Message | CallbackQuery) -> None:
is_callback = False
manager = get_manager()
try:
# Получаем всех админов из БД
db_admins = await manager.repo.get_admins()
# Получаем статистику
stats = await manager.get_stats()
output = f'{tg_emoji("4960891456869893259")} <b>СПИСОК АДМИНИСТРАТОРОВ</b>\n\n'
# === ФОРМИРУЕМ ВЫВОД ===
output = "👥 <b>СПИСОК АДМИНИСТРАТОРОВ</b>\n\n"
# Владельцы (OWNER_ID)
output += "👑 <b>Владельцы бота</b> (полные права):\n"
# ВЛАДЕЛЬЦЫ
output += f'{tg_emoji("4963343509533754468")} <b>Владельцы бота</b> (полные права):\n'
for owner_id in settings.OWNER_ID:
output += f'├─ <a href="tg://user?id={owner_id}">{owner_id}</a>\n'
output += "\n"
display_name = await get_user_display_name(owner_id)
output += f'├─ <a href="tg://user?id={owner_id}">{display_name}</a>\n'
output += '\n'
# Администраторы из БД
# АДМИНИСТРАТОРЫ
if db_admins:
output += f"⚙️ <b>Администраторы</b> ({len(db_admins)}):\n"
output += f'{tg_emoji("4961064956368782417")} <b>Администраторы</b> ({len(db_admins)}):\n'
for admin_id in sorted(db_admins):
output += f'├─ <a href="tg://user?id={admin_id}">{admin_id}</a>\n'
output += "\n"
output += "📋 <b>Права администраторов:</b>\n"
output += "├─ Управление банвордами\n"
output += "├─ Просмотр статистики\n"
output += "├─ Активация режимов модерации\n"
output += "└─ Все команды бота (кроме управления админами)\n\n"
display_name = await get_user_display_name(admin_id)
output += f'├─ <a href="tg://user?id={admin_id}">{display_name}</a>\n'
output += '\n'
output += f'{tg_emoji("4961106084975608869")} <b>Права администраторов:</b>\n'
output += '├─ Управление банвордами\n'
output += '├─ Просмотр статистики\n'
output += '├─ Активация режимов модерации\n'
output += '└─ Все команды бота\n\n'
else:
output += "⚙️ <b>Администраторы:</b>\n"
output += "└─ <i>Нет дополнительных администраторов</i>\n\n"
output += f'{tg_emoji("4961064956368782417")} <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 += f'{tg_emoji("4961061266991875258")} <b>Итого:</b> {total_admins} администратор(ов)\n\n'
# Команды управления
output += "🔧 <b>Управление:</b>\n"
output += "/addadmin <code>ID</code> — добавить админа\n"
output += "/remadmin <code>ID</code> — удалить админа\n\n"
output += f'{tg_emoji("4961027057577362562")} <b>Управление:</b>\n'
output += '• <b>/adminhelp</b> — помощь по командам админов\n'
output += '• <code>/addadmin</code> <code>ID</code> — добавить админа\n'
output += '• <code>/remadmin</code> <code>ID</code> — удалить админа\n\n'
output += f'{tg_emoji("4961186405159011104")} <i>Только владельцы могут управлять администраторами</i>'
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("✅ Список обновлён")
await message.edit_text(text=output, parse_mode='HTML', reply_markup=keyboard)
await update.answer(f'{tg_emoji("4963010134172239128")} Список обновлён')
else:
await message.answer(
text=output,
parse_mode="HTML",
reply_markup=keyboard
)
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Попробуйте позже"
logger.error(f'Ошибка получения списка администраторов: {e}', log_type='ADMIN_MGMT')
error_text = f'{tg_emoji("4961187972822074653")} <b>Ошибка загрузки списка</b>\n\nПопробуйте позже'
if is_callback:
await update.answer(" Ошибка загрузки", show_alert=True)
await update.answer(f'{tg_emoji("4961187972822074653")} Ошибка загрузки', show_alert=True)
else:
await message.answer(error_text, parse_mode="HTML")
await message.answer(error_text, parse_mode='HTML')
# ================= ВСПОМОГАТЕЛЬНЫЕ CALLBACK =================
@router.callback_query(F.data == "admin:help_add")
@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>"
f'{tg_emoji("4963469772982322370")} <b>Как добавить администратора?</b>\n\n'
f'{tg_emoji("4960889107522782272")} Узнайте Telegram ID пользователя\n'
' • Используйте команду <b>/id</b> или бота @userinfobot\n'
f'{tg_emoji("4960889107522782272")} Выполните команду:\n'
' <code>/addadmin ID</code>\n\n'
'Пример:\n'
'<code>/addadmin 123456789</code>'
)
await callback.answer()
await callback.message.answer(text, parse_mode="HTML")
await callback.message.answer(text, parse_mode='HTML')
@router.message(Command(*COMMANDS.get("adminhelp", ["adminhelp"]), prefix=settings.PREFIX, ignore_case=True),
@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"
"└─ Все действия логируются"
f'{tg_emoji("4960891456869893259")} <b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ</b>\n\n'
f'{tg_emoji("4963401727815451692")} <b>Уровни доступа:</b>\n\n'
f'{tg_emoji("4963343509533754468")} <b>Владельцы</b> (OWNER_ID):\n'
'├─ Все права администратора\n'
'├─ Управление другими админами\n'
'└─ Указываются в конфигурации\n\n'
f'{tg_emoji("4961064956368782417")} <b>Администраторы:</b>\n'
'├─ Управление банвордами\n'
'├─ Просмотр статистики\n'
'├─ Активация режимов модерации\n'
'└─ Не могут управлять админами\n\n'
f'{tg_emoji("4963241130398319816")} <b>Команды:</b>\n'
'• <b>/adminhelp</b> — помощь по командам админов\n'
'• <b>/listadmins</b> — список всех админов\n'
'• <code>/addadmin</code> <code>ID</code> — добавить админа\n'
'• <code>/remadmin</code> <code>ID</code> — удалить админа\n\n'
f'{tg_emoji("4961186405159011104")} <b>Как узнать ID пользователя?</b>\n'
'• Используйте команду <b>/id</b> или бота @userinfobot\n'
'• Или попросите пользователя написать боту\n'
'• ID отображается в логах бота\n\n'
f'{tg_emoji("4963024861615096794")} <b>Важно:</b>\n'
'├─ Нельзя удалить владельца\n'
'├─ Нельзя удалить самого себя\n'
'└─ Все действия логируются'
)
await message.answer(text, parse_mode="HTML")
await message.answer(text, parse_mode='HTML')
@router.message(Command(*COMMANDS.get("checkadmin", ["checkadmin"]), prefix=settings.PREFIX, ignore_case=True),
@router.message(Command(*COMMANDS.get('checkadmin', ['checkadmin']), prefix=settings.PREFIX, ignore_case=True),
IsSuperAdmin())
@log_action(action_name="CHECK_ADMIN")
@log_action(action_name='CHECK_ADMIN')
async def check_admin_cmd(message: Message) -> None:
"""
Проверяет, является ли пользователь администратором.
Использование: /checkadmin <ID>
"""
success, result = parse_user_id(message.text, "checkadmin")
success, result = parse_user_id(message.text, 'checkadmin')
if not success:
await message.answer(result, parse_mode="HTML")
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"
text = f'{tg_emoji("4961092195051373778")} <b>Проверка пользователя</b>\n\n'
text += f'{tg_emoji("4961064956368782417")} ID: <code>{user_id}</code>\n\n'
if is_owner:
text += "👑 Статус: <b>Владелец бота</b>\n"
text += " Полные права администратора\n"
text += " Может управлять админами"
text += f'{tg_emoji("4963343509533754468")} Статус: <b>Владелец бота</b>\n'
text += f'{tg_emoji("4963010134172239128")} Полные права администратора\n'
text += f'{tg_emoji("4963010134172239128")} Может управлять админами'
elif is_db_admin:
text += "⚙️ Статус: <b>Администратор</b>\n"
text += " Доступ к командам бота\n"
text += " Не может управлять админами"
text += f'{tg_emoji("4961064956368782417")} Статус: <b>Администратор</b>\n'
text += f'{tg_emoji("4963010134172239128")} Доступ к командам бота\n'
text += f'{tg_emoji("4961187972822074653")} Не может управлять админами'
else:
text += "👤 Статус: <b>Обычный пользователь</b>\n"
text += " Нет прав администратора\n\n"
text += f"Добавить в админы: <code>/addadmin {user_id}</code>"
text += f'{tg_emoji("4961064956368782417")} Статус: <b>Обычный пользователь</b>\n'
text += f'{tg_emoji("4961187972822074653")} Нет прав администратора\n\n'
text += f'Добавить в админы: <code>/addadmin {user_id}</code>'
await message.answer(text, parse_mode="HTML")
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")
logger.error(f'Ошибка проверки администратора: {e}', log_type='ADMIN_MGMT')
await message.answer(f'{tg_emoji("4961187972822074653")} <b>Ошибка проверки</b>', parse_mode='HTML')

View File

@@ -0,0 +1,330 @@
"""
Команда /settings - управление настройками БЕЗ .env
ADMIN_CHAT_ID, ADMIN_THREAD_ID, REPORT_CHAT_ID, REPORT_THREAD_ID
"""
from aiogram import Router, F
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.exceptions import TelegramBadRequest
from middleware.loggers import logger
from bot.filters.admin import IsAdmin
from database import get_manager
__all__ = ("router",)
router: Router = Router(name="bot_settings_router")
# ======================================================================
# FSM STATES
# ======================================================================
class BotSettingsStates(StatesGroup):
"""Состояния для редактирования настроек бота"""
waiting_admin_chat = State()
waiting_admin_thread = State()
waiting_report_chat = State()
waiting_report_thread = State()
# ======================================================================
# MAIN MENU
# ======================================================================
def _format_chat_id(chat_id: str | None) -> str:
"""Форматирует ID чата для отображения"""
if chat_id is None:
return "Не установлен"
return f"✅ <code>{chat_id}</code>"
def create_settings_menu() -> InlineKeyboardBuilder:
"""Главное меню настроек"""
ikb = InlineKeyboardBuilder()
ikb.button(text="📢 Админ-чат", callback_data="settings:admin_chat")
ikb.button(text="🧵 Топик админ-чата", callback_data="settings:admin_thread")
ikb.button(text="📊 Чат репортов", callback_data="settings:report_chat")
ikb.button(text="🧵 Топик репортов", callback_data="settings:report_thread")
ikb.button(text="🔄 Обновить", callback_data="settings:refresh")
ikb.button(text="❌ Закрыть", callback_data="settings:close")
ikb.adjust(2)
return ikb
def cancel_keyboard():
"""Клавиатура с кнопкой 'Назад' для окон ввода"""
ikb = InlineKeyboardBuilder()
ikb.button(text="◀️ Назад", callback_data="settings:cancel")
return ikb.as_markup()
# ======================================================================
# MAIN HANDLER
# ======================================================================
@router.message(Command("settings"), IsAdmin())
async def settings_cmd(message: Message, state: FSMContext) -> None:
"""Главная команда /settings"""
await state.clear()
await show_settings_menu(message)
async def show_settings_menu(message_or_callback: Message | CallbackQuery) -> None:
"""Показывает меню настроек (отправляет новое сообщение или редактирует существующее)"""
manager = get_manager()
current = await manager.get_bot_settings()
text = (
"⚙️ <b>НАСТРОЙКИ БОТА</b>\n\n"
"📢 <b>Админ-чат:</b> " + _format_chat_id(current.get('admin_chat_id')) + "\n"
"🧵 <b>Топик админ:</b> " + _format_chat_id(current.get('admin_thread_id')) + "\n\n"
"📊 <b>Чат репортов:</b> " + _format_chat_id(current.get('report_chat_id')) + "\n"
"🧵 <b>Топик репортов:</b> " + _format_chat_id(current.get('report_thread_id')) + "\n\n"
"💡 Используйте @userinfobot для получения ID чатов\n"
"💡 Для топиков: ID из сообщения в топике"
)
markup = create_settings_menu().as_markup()
if isinstance(message_or_callback, Message):
await message_or_callback.answer(text, reply_markup=markup, parse_mode="HTML")
else:
try:
await message_or_callback.message.edit_text(text, reply_markup=markup, parse_mode="HTML")
except TelegramBadRequest as e:
if "message is not modified" in str(e):
await message_or_callback.answer("🔄 Нет изменений")
else:
raise
# ======================================================================
# CALLBACK HANDLERS
# ======================================================================
@router.callback_query(F.data == "settings:refresh")
async def refresh_settings(callback: CallbackQuery, state: FSMContext) -> None:
"""Обновляет меню (с защитой от MessageNotModified)"""
await show_settings_menu(callback)
@router.callback_query(F.data == "settings:close")
async def close_settings(callback: CallbackQuery, state: FSMContext) -> None:
"""Закрывает меню"""
await state.clear()
try:
await callback.message.delete()
except:
pass
await callback.answer("❌ Закрыто")
@router.callback_query(F.data == "settings:cancel")
async def cancel_edit(callback: CallbackQuery, state: FSMContext) -> None:
"""Возврат в главное меню без сохранения"""
await state.clear()
await show_settings_menu(callback)
@router.callback_query(F.data == "settings:admin_chat")
async def edit_admin_chat(callback: CallbackQuery, state: FSMContext) -> None:
"""Редактирование админ-чата"""
await state.set_state(BotSettingsStates.waiting_admin_chat)
await callback.message.edit_text(
"📢 <b>АДМИН-ЧАТ</b>\n\n"
"Отправьте ID чата для уведомлений:\n"
"<code>Пример: -1003764219200</code>\n\n"
"Для отключения: <code>null</code>\n\n"
"Или нажмите кнопку ниже для возврата в меню.",
parse_mode="HTML",
reply_markup=cancel_keyboard()
)
await callback.answer()
@router.callback_query(F.data == "settings:admin_thread")
async def edit_admin_thread(callback: CallbackQuery, state: FSMContext) -> None:
"""Редактирование топика админ-чата"""
await state.set_state(BotSettingsStates.waiting_admin_thread)
await callback.message.edit_text(
"🧵 <b>ТОПИК АДМИН-ЧАТА</b>\n\n"
"Отправьте ID топика:\n"
"<code>Пример: 1</code>\n\n"
"Для отключения: <code>null</code>\n\n"
"Или нажмите кнопку ниже для возврата в меню.",
parse_mode="HTML",
reply_markup=cancel_keyboard()
)
await callback.answer()
@router.callback_query(F.data == "settings:report_chat")
async def edit_report_chat(callback: CallbackQuery, state: FSMContext) -> None:
"""Редактирование чата репортов"""
await state.set_state(BotSettingsStates.waiting_report_chat)
await callback.message.edit_text(
"📊 <b>ЧАТ РЕПОРТОВ</b>\n\n"
"Отправьте ID чата для репортов:\n"
"<code>Пример: -1003764219200</code>\n\n"
"Для отключения: <code>null</code>\n\n"
"Или нажмите кнопку ниже для возврата в меню.",
parse_mode="HTML",
reply_markup=cancel_keyboard()
)
await callback.answer()
@router.callback_query(F.data == "settings:report_thread")
async def edit_report_thread(callback: CallbackQuery, state: FSMContext) -> None:
"""Редактирование топика репортов"""
await state.set_state(BotSettingsStates.waiting_report_thread)
await callback.message.edit_text(
"🧵 <b>ТОПИК РЕПОРТОВ</b>\n\n"
"Отправьте ID топика:\n"
"<code>Пример: 1</code>\n\n"
"Для отключения: <code>null</code>\n\n"
"Или нажмите кнопку ниже для возврата в меню.",
parse_mode="HTML",
reply_markup=cancel_keyboard()
)
await callback.answer()
# ======================================================================
# MESSAGE HANDLERS (FSM)
# ======================================================================
@router.message(BotSettingsStates.waiting_admin_chat, IsAdmin())
async def process_admin_chat(message: Message, state: FSMContext) -> None:
text = message.text.strip()
if text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
if text == "null":
value = None
else:
try:
value = int(text)
if not str(value).startswith('-'):
raise ValueError("ID чата должен начинаться с минуса")
except ValueError:
await message.answer("❌ Неверный формат. Пример: <code>-1003764219200</code>", parse_mode="HTML")
return
manager = get_manager()
success = await manager.set_bot_setting("admin_chat_id", str(value) if value else None)
await state.clear()
if success:
# Показываем обновлённое главное меню
await show_settings_menu(message)
# Удаляем сообщение с вводом
try:
await message.delete()
except:
pass
else:
await message.answer("❌ Ошибка сохранения", parse_mode="HTML")
@router.message(BotSettingsStates.waiting_admin_thread, IsAdmin())
async def process_admin_thread(message: Message, state: FSMContext) -> None:
text = message.text.strip()
if text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
if text == "null":
value = None
else:
try:
value = int(text)
if value < 1:
raise ValueError("ID топика должен быть > 0")
except ValueError:
await message.answer("❌ Неверный формат. Пример: <code>1</code>", parse_mode="HTML")
return
manager = get_manager()
success = await manager.set_bot_setting("admin_thread_id", str(value) if value else None)
await state.clear()
if success:
await show_settings_menu(message)
try:
await message.delete()
except:
pass
else:
await message.answer("❌ Ошибка сохранения", parse_mode="HTML")
@router.message(BotSettingsStates.waiting_report_chat, IsAdmin())
async def process_report_chat(message: Message, state: FSMContext) -> None:
text = message.text.strip()
if text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
if text == "null":
value = None
else:
try:
value = int(text)
if not str(value).startswith('-'):
raise ValueError("ID чата должен начинаться с минуса")
except ValueError:
await message.answer("❌ Неверный формат. Пример: <code>-1003764219200</code>", parse_mode="HTML")
return
manager = get_manager()
success = await manager.set_bot_setting("report_chat_id", str(value) if value else None)
await state.clear()
if success:
await show_settings_menu(message)
try:
await message.delete()
except:
pass
else:
await message.answer("❌ Ошибка сохранения", parse_mode="HTML")
@router.message(BotSettingsStates.waiting_report_thread, IsAdmin())
async def process_report_thread(message: Message, state: FSMContext) -> None:
text = message.text.strip()
if text == "/cancel":
await state.clear()
await message.answer("❌ Отменено")
return
if text == "null":
value = None
else:
try:
value = int(text)
if value < 1:
raise ValueError("ID топика должен быть > 0")
except ValueError:
await message.answer("❌ Неверный формат. Пример: <code>1</code>", parse_mode="HTML")
return
manager = get_manager()
success = await manager.set_bot_setting("report_thread_id", str(value) if value else None)
await state.clear()
if success:
await show_settings_menu(message)
try:
await message.delete()
except:
pass
else:
await message.answer("❌ Ошибка сохранения", parse_mode="HTML")
@router.message(Command("cancel"))
async def cancel_settings(message: Message, state: FSMContext) -> None:
"""Глобальный cancel"""
await state.clear()
await message.answer("✅ Настройки отменены")

View File

@@ -0,0 +1,48 @@
# ======================================================================
# CLOSE / CANCEL
# ======================================================================
from __future__ import annotations
from typing import Optional, Tuple, Dict, Any, List
from aiogram import Router, F, Bot
from aiogram.types import Message, CallbackQuery
from aiogram.filters import Command
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.utils.markdown import hide_link
from aiogram.exceptions import TelegramBadRequest, TelegramForbiddenError
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from configs import settings, COMMANDS
from database import get_manager, AutoComment
from middleware.loggers import logger
from bot.filters.admin import IsAdmin
from bot.utils import log_action, tg_emoji
__all__ = ("router",)
CMD: str = "cancel"
router: Router = Router(name="channel_comments_router")
@router.callback_query(F.data == "menu:close")
async def close_menu_callback(callback: CallbackQuery, state: FSMContext) -> None:
await state.clear()
try:
if callback.message:
await callback.message.delete()
except TelegramBadRequest:
pass
await callback.answer("❌ Меню закрыто")
@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 cancel_handler(message: Message, state: FSMContext) -> None:
current_state = await state.get_state()
if current_state is None:
await message.answer("❌ Нечего отменять")
return
await state.clear()
await message.answer("✅ Действие отменено")

View File

@@ -14,23 +14,28 @@ __all__ = ("router",)
router: Router = Router(name="emoji_extractor_router")
MAX_MSG_LEN = 3800 # Безопасный лимит (4096 - запас)
SEPARATOR = "\n" + "" * 30 + "\n\n"
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
def _utf16_slice(text: str, offset: int, length: int) -> str:
"""
Корректно извлекает подстроку с учётом UTF-16 смещений Telegram API.
Telegram передаёт offset/length в UTF-16 code units, а не в Unicode codepoints.
"""
encoded = text.encode("utf-16-le")
return encoded[offset * 2 : (offset + length) * 2].decode("utf-16-le")
def extract_custom_emojis(message: Message) -> list[dict]:
"""
Извлекает все кастомные эмодзи из сообщения.
Args:
message: Сообщение для анализа
Извлекает все кастомные эмодзи из сообщения (текст + подпись).
Returns:
Список словарей с информацией об эмодзи
Список словарей: {"char": str, "id": str, "offset": int}
"""
if not message.entities and not message.caption_entities:
return []
# Определяем текст и entities
text = message.text or message.caption
entities = message.entities or message.caption_entities
@@ -38,44 +43,76 @@ def extract_custom_emojis(message: Message) -> list[dict]:
return []
custom_emojis = []
for entity in entities:
if entity.type == "custom_emoji":
# Извлекаем символ эмодзи
emoji_char = text[entity.offset:entity.offset + entity.length]
emoji_char = _utf16_slice(text, entity.offset, entity.length)
custom_emojis.append({
"char": emoji_char,
"id": entity.custom_emoji_id,
"offset": entity.offset
"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;")
.replace("<", "&lt;")
.replace(">", "&gt;")
)
def _build_emoji_block(idx: int, emoji_data: dict, is_last: bool) -> str:
"""Формирует текстовый блок для одного эмодзи."""
emoji_char = emoji_data["char"]
emoji_id = emoji_data["id"]
html_code = format_emoji_html(emoji_char, emoji_id)
html_escaped = escape_html(html_code)
block = (
f"<b>{idx}.</b> Эмодзи: {emoji_char}\n"
f"📋 <b>ID:</b> <code>{emoji_id}</code>\n\n"
f"📝 <b>HTML-код:</b>\n"
f"<code>{html_escaped}</code>\n\n"
f"🎨 <b>Превью:</b> {html_code}\n"
)
if not is_last:
block += SEPARATOR
return block
def build_pages(custom_emojis: list[dict]) -> list[str]:
"""
Разбивает список эмодзи на страницы, каждая не длиннее MAX_MSG_LEN.
Возвращает список готовых HTML-строк для отправки.
"""
total = len(custom_emojis)
pages: list[str] = []
current_page = ""
for idx, emoji_data in enumerate(custom_emojis, 1):
is_last = (idx == total)
block = _build_emoji_block(idx, emoji_data, is_last)
if current_page and len(current_page) + len(block) > MAX_MSG_LEN:
pages.append(current_page)
current_page = block
else:
current_page += block
if current_page:
pages.append(current_page)
return pages
# ================= КОМАНДА /EMOJI =================
@router.message(
@@ -83,14 +120,6 @@ def escape_html(text: str) -> str:
IsAdmin()
)
async def emoji_extractor_cmd(message: Message) -> None:
"""
Извлекает кастомные эмодзи из сообщения.
Доступно только администраторам.
Использование: /emoji (в ответ на сообщение)
"""
# Проверяем, что команда в ответ на сообщение
if not message.reply_to_message:
await message.answer(
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
@@ -98,66 +127,57 @@ async def emoji_extractor_cmd(message: Message) -> None:
"1. Ответьте на сообщение с премиум эмодзи\n"
"2. Напишите <code>/emoji</code>\n\n"
"💡 <i>Бот извлечёт все кастомные эмодзи и покажет HTML-код</i>",
parse_mode="HTML"
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"
parse_mode="HTML",
)
return
# === ФОРМИРУЕМ ОТВЕТ ===
total = len(custom_emojis)
pages = build_pages(custom_emojis)
total_pages = len(pages)
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()
)
for page_num, page_content in enumerate(pages, 1):
# Заголовок только на первой странице
if page_num == 1:
header = f"✨ <b>НАЙДЕНО ЭМОДЗИ: {total}</b>\n\n"
else:
header = f"✨ <b>ПРОДОЛЖЕНИЕ ({page_num}/{total_pages})</b>\n\n"
# Подвал только на последней странице
footer = (
"\n\n💡 <i>Скопируйте HTML-код и используйте в своих сообщениях</i>"
if page_num == total_pages
else ""
)
# Кнопка закрытия только на последней странице
markup = ikb.as_markup() if page_num == total_pages else None
await message.answer(
text=header + page_content + footer,
parse_mode="HTML",
reply_markup=markup,
)
logger.info(
f"Извлечено {len(custom_emojis)} кастомных эмодзи админом {message.from_user.id}",
log_type="EMOJI_EXTRACT"
f"Извлечено {total} кастомных эмодзи ({total_pages} стр.) "
f"админом {message.from_user.id}",
log_type="EMOJI_EXTRACT",
)
except Exception as e:
@@ -165,7 +185,7 @@ async def emoji_extractor_cmd(message: Message) -> None:
await message.answer(
"❌ <b>Ошибка извлечения эмодзи</b>\n\n"
"Попробуйте позже или обратитесь к разработчику.",
parse_mode="HTML"
parse_mode="HTML",
)
@@ -173,7 +193,6 @@ async def emoji_extractor_cmd(message: Message) -> None:
@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("✅ Закрыто")
@@ -189,9 +208,6 @@ async def emoji_close_callback(callback) -> None:
IsAdmin()
)
async def emoji_help_cmd(message: Message) -> None:
"""
Справка по работе с кастомными эмодзи.
"""
text = (
"🎨 <b>РАБОТА С КАСТОМНЫМИ ЭМОДЗИ</b>\n\n"
"📝 <b>Команда /emoji</b>\n"
@@ -201,15 +217,11 @@ async def emoji_help_cmd(message: Message) -> None:
"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"
'<code>&lt;tg-emoji emoji-id="ID"&gt;fallback&lt;/tg-emoji&gt;</code>\n\n'
"⚠️ <b>Важно:</b>\n"
"├─ Используйте <code>parse_mode=\"HTML\"</code>\n"
'├─ Используйте <code>parse_mode="HTML"</code>\n'
"├─ Пользователи без Premium видят fallback\n"
"└─ Работает только с кастомными эмодзи\n\n"
"💡 <i>Попробуйте отправить эмодзи и ответить командой /emoji</i>"
)
await message.answer(text, parse_mode="HTML")

View File

@@ -9,9 +9,9 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from configs import settings, COMMANDS
from middleware.loggers import logger
__all__ = ("router",)
__all__ = ('router',)
router: Router = Router(name="user_id_router")
router: Router = Router(name='user_id_router')
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
@@ -19,13 +19,13 @@ router: Router = Router(name="user_id_router")
def get_close_keyboard():
"""Создаёт клавиатуру с кнопкой закрытия"""
ikb = InlineKeyboardBuilder()
ikb.button(text="✖️ Закрыть", callback_data="id_close")
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))
@router.message(Command(*COMMANDS.get('id', ['id']), prefix=settings.PREFIX, ignore_case=True))
async def id_cmd(message: Message) -> None:
"""
Показывает информацию о вашем Telegram аккаунте.
@@ -37,12 +37,12 @@ async def id_cmd(message: Message) -> None:
user = message.from_user
if not user:
await message.answer("Не удалось получить информацию о пользователе")
await message.answer('Не удалось получить информацию о пользователе')
return
# === ФОРМИРУЕМ ИНФОРМАЦИЮ ===
output = "👤 <b>ИНФОРМАЦИЯ О ВАС</b>\n\n"
output = '<tg-emoji emoji-id="4961064956368782417">💠</tg-emoji> <b>ИНФОРМАЦИЯ О ВАС</b>\n\n'
# Имя
full_name_parts = []
@@ -51,28 +51,28 @@ async def id_cmd(message: Message) -> None:
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"
full_name = ' '.join(full_name_parts) if full_name_parts else 'Не указано'
output += f'<tg-emoji emoji-id="4960791319707387164">💠</tg-emoji> <b>Имя:</b> {full_name}\n'
# Username
if user.username:
output += f"🔗 <b>Username:</b> @{user.username}\n"
output += f'<tg-emoji emoji-id="4961200307968148582">💠</tg-emoji> <b>Username:</b> @{user.username}\n'
else:
output += f"🔗 <b>Username:</b> <i>не установлен</i>\n"
output += '<tg-emoji emoji-id="4961200307968148582">💠</tg-emoji> <b>Username:</b> <i>не установлен</i>\n'
# ID
output += f"🆔 <b>ID:</b> <code>{user.id}</code>\n\n"
output += f'<tg-emoji emoji-id="4961121396534019447">💠</tg-emoji> <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"
output += '🤖 <b>Тип:</b> Бот\n'
elif getattr(user, 'is_premium', False):
output += '<tg-emoji emoji-id="4961075019477156700">💠</tg-emoji> <b>Тип:</b> Premium пользователь\n'
else:
output += f"👥 <b>Тип:</b> Обычный пользователь\n"
output += '👥 <b>Тип:</b> Обычный пользователь\n'
# Дополнительная информация
output += "\n📊 <b>Дополнительно:</b>\n"
output += '\n<tg-emoji emoji-id="4961141003059725568">💠</tg-emoji> <b>Дополнительно:</b>\n'
# Язык
if user.language_code:
@@ -86,36 +86,33 @@ async def id_cmd(message: Message) -> None:
'it': '🇮🇹 Italiano',
'pt': '🇵🇹 Português',
}
language = language_names.get(user.language_code, f"🌐 {user.language_code.upper()}")
output += f"├─ Язык: {language}\n"
language = language_names.get(user.language_code, f'🌐 {user.language_code.upper()}')
output += f'├─ Язык: {language}\n'
# Информация о чате
if message.chat.type == "private":
output += f"├─ Чат: 💬 Личные сообщения\n"
if message.chat.type == 'private':
output += '├─ Чат: 💬 Личные сообщения\n'
else:
chat_title = message.chat.title or "Без названия"
chat_title = message.chat.title or 'Без названия'
chat_types = {
"group": "👥 Группа",
"supergroup": "👥 Супергруппа",
"channel": "📢 Канал"
'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"
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"
output += f'├─ Участников: {member_count}\n'
except Exception as e:
logger.debug(f"Не удалось получить количество участников: {e}", log_type="USER_ID")
logger.debug(f'Не удалось получить количество участников: {e}', log_type='USER_ID')
# Message ID
output += f"└─ Message ID: <code>{message.message_id}</code>\n\n"
# Подсказка
output += "💡 <i>Эту информацию видите только вы</i>"
output += f'└─ Message ID: <code>{message.message_id}</code>\n\n'
# Клавиатура
keyboard = get_close_keyboard()
@@ -124,33 +121,33 @@ async def id_cmd(message: Message) -> None:
try:
await message.answer(
text=output,
parse_mode="HTML",
parse_mode='HTML',
reply_markup=keyboard
)
logger.debug(f"Команда /id от пользователя {user.id}", log_type="USER_ID")
logger.debug(f'Команда /id от пользователя {user.id}', log_type='USER_ID')
except Exception as e:
logger.error(f"Ошибка отправки информации о пользователе: {e}", log_type="ERROR")
await message.answer("❌ Произошла ошибка при получении информации")
logger.error(f'Ошибка отправки информации о пользователе: {e}', log_type='ERROR')
await message.answer('❌ Произошла ошибка при получении информации')
# ================= ОБРАБОТЧИК КНОПКИ ЗАКРЫТИЯ =================
@router.callback_query(F.data == "id_close")
@router.callback_query(F.data == 'id_close')
async def id_close_callback(callback: CallbackQuery) -> None:
"""Закрывает (удаляет) сообщение с информацией"""
try:
await callback.message.delete()
await callback.answer("✅ Закрыто")
await callback.answer('✅ Закрыто')
except Exception as e:
logger.error(f"Ошибка удаления сообщения ID: {e}", log_type="ERROR")
await callback.answer("Не удалось удалить сообщение", show_alert=True)
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))
@router.message(Command(*COMMANDS.get('myid', ['myid']), prefix=settings.PREFIX, ignore_case=True))
async def myid_cmd(message: Message) -> None:
"""
Быстрый просмотр вашего ID.
@@ -160,21 +157,21 @@ async def myid_cmd(message: Message) -> None:
user = message.from_user
if not user:
await message.answer("Не удалось получить ID")
await message.answer('Не удалось получить ID')
return
# Короткий ответ
text = f"🆔 Ваш ID: <code>{user.id}</code>"
text = f'<tg-emoji emoji-id="4961121396534019447">💠</tg-emoji> Ваш ID: <code>{user.id}</code>'
if user.username:
text += f"\n🔗 Username: @{user.username}"
text += f'\n<tg-emoji emoji-id="4961200307968148582">💠</tg-emoji> Username: @{user.username}'
await message.answer(text, parse_mode="HTML")
await message.answer(text, parse_mode='HTML')
# ================= КОМАНДА /CHATID =================
@router.message(Command(*COMMANDS.get("chatid", ["chatid"]), prefix=settings.PREFIX, ignore_case=True))
@router.message(Command(*COMMANDS.get('chatid', ['chatid']), prefix=settings.PREFIX, ignore_case=True))
async def chatid_cmd(message: Message) -> None:
"""
Показывает ID текущего чата.
@@ -183,39 +180,39 @@ async def chatid_cmd(message: Message) -> None:
"""
chat = message.chat
output = "💬 <b>ИНФОРМАЦИЯ О ЧАТЕ</b>\n\n"
output = '💬 <b>ИНФОРМАЦИЯ О ЧАТЕ</b>\n\n'
# Тип чата
chat_types = {
"private": "💬 Личные сообщения",
"group": "👥 Группа",
"supergroup": "👥 Супергруппа",
"channel": "📢 Канал"
'private': '💬 Личные сообщения',
'group': '👥 Группа',
'supergroup': '👥 Супергруппа',
'channel': '📢 Канал'
}
chat_type = chat_types.get(chat.type, "💬 Чат")
chat_type = chat_types.get(chat.type, '💬 Чат')
output += f"📝 <b>Тип:</b> {chat_type}\n"
output += f'<tg-emoji emoji-id="4960791319707387164">💠</tg-emoji> <b>Тип:</b> {chat_type}\n'
if chat.title:
output += f"📌 <b>Название:</b> {chat.title}\n"
output += f'📌 <b>Название:</b> {chat.title}\n'
if chat.username:
output += f"🔗 <b>Username:</b> @{chat.username}\n"
output += f'<tg-emoji emoji-id="4961200307968148582">💠</tg-emoji> <b>Username:</b> @{chat.username}\n'
output += f"🆔 <b>Chat ID:</b> <code>{chat.id}</code>\n"
output += f'<tg-emoji emoji-id="4961121396534019447">💠</tg-emoji> <b>Chat ID:</b> <code>{chat.id}</code>\n'
# Дополнительная информация для групп
if chat.type in ["group", "supergroup"]:
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"
output += f'👥 <b>Участников:</b> {member_count}\n'
except Exception as e:
logger.debug(f"Не удалось получить количество участников: {e}", log_type="USER_ID")
logger.debug(f'Не удалось получить количество участников: {e}', log_type='USER_ID')
keyboard = get_close_keyboard()
await message.answer(
text=output,
parse_mode="HTML",
parse_mode='HTML',
reply_markup=keyboard
)

View File

@@ -5,6 +5,7 @@ from aiogram import Router, F
from aiogram.filters import Command
from aiogram.types import Message, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiogram.exceptions import TelegramBadRequest
from bot.filters.admin import IsAdmin
from configs import settings, COMMANDS
@@ -123,7 +124,7 @@ async def format_banwords_list(page: int = 0) -> str:
# === КОНФЛИКТНЫЕ ПРАВИЛА ===
if conflict_words or conflict_lemmas:
output += "⚔️ <b>КОНФЛИКТНЫЕ ПРАВИЛА:</b>\n"
output += "<i>(работают только в режиме /stopconflict)</i>\n\n"
output += "<i>(работают только в режиме <code>/stopconflict</code> <code>время</code>)</i>\n\n"
if conflict_words:
output += f"📝 <b>Конфликтные слова</b> ({len(conflict_words)}):\n"
@@ -188,9 +189,7 @@ async def listwords_cmd(update: Message | CallbackQuery) -> None:
"""
Обработчик команды /listwords.
Отображает список всех правил модерации с разбивкой по категориям.
Доступно только администраторам.
Args:
update: Message или CallbackQuery
"""
@@ -214,12 +213,18 @@ async def listwords_cmd(update: Message | CallbackQuery) -> None:
keyboard = get_refresh_kb(page)
if is_callback:
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=keyboard
)
await update.answer("✅ Список обновлён")
try:
await message.edit_text(
text=text,
parse_mode="HTML",
reply_markup=keyboard
)
await update.answer("✅ Список обновлён")
except TelegramBadRequest as e:
if 'message is not modified' in str(e).lower():
await update.answer('✅ Список уже актуален')
return
raise # Другие ошибки пробрасываем
else:
await message.answer(
text=text,
@@ -233,6 +238,6 @@ async def listwords_cmd(update: Message | CallbackQuery) -> None:
error_text = "❌ <b>Ошибка загрузки списка</b>\n\nПопробуйте позже"
if is_callback:
await update.answer("❌ Ошибка загрузки", show_alert=True)
await update.answer(f"❌ Ошибка загрузки: {e}", show_alert=True)
else:
await message.answer(error_text, parse_mode="HTML")

View File

@@ -2,11 +2,12 @@
Обработчики команды /report для пользователей
"""
from datetime import datetime
from aiogram import Router, F
from aiogram.exceptions import TelegramBadRequest
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
@@ -18,29 +19,13 @@ __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)
@@ -49,11 +34,9 @@ def format_user(user: User) -> str:
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
return full_name
def format_datetime(dt: datetime) -> str:
@@ -72,7 +55,8 @@ def get_report_keyboard(
chat_id: int,
message_id: int,
reported_user_id: int,
report_id: str
report_id: str,
message_thread_id: int | None = None
) -> InlineKeyboardBuilder:
"""
Создает клавиатуру для репорта.
@@ -82,17 +66,19 @@ def get_report_keyboard(
message_id: ID сообщения
reported_user_id: ID пользователя, на которого пожаловались
report_id: Уникальный ID репорта
message_thread_id: ID топика (если есть)
"""
ikb = InlineKeyboardBuilder()
# Кнопки действий
thread_id = message_thread_id if message_thread_id is not None else 0
ikb.button(
text="🚫 Забанить",
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}"
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}:{thread_id}"
)
ikb.button(
text="🗑 Удалить",
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}"
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}:{thread_id}"
)
ikb.button(
text="✅ Закрыть",
@@ -115,17 +101,10 @@ async def report_cmd(message: Message) -> None:
"""
Отправляет жалобу на сообщение администраторам.
Доступно всем пользователям.
Использование:
/report — в ответ на сообщение
/report <причина> — в ответ на сообщение с указанием причины
Пример:
/report спам
/report оскорбления
"""
# Проверяем, что команда в ответ на сообщение
if not message.reply_to_message:
await message.answer(
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
@@ -133,7 +112,7 @@ async def report_cmd(message: Message) -> None:
"1. Ответьте на сообщение нарушителя\n"
"2. Напишите <code>/report</code> или <code>/report причина</code>\n\n"
"Пример: <code>/report спам</code>",
parse_mode="HTML"
parse_mode="HTML",
)
return
@@ -141,103 +120,104 @@ async def report_cmd(message: Message) -> None:
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")
await message.answer(
"❌ <b>Ошибка получения данных пользователя</b>",
parse_mode="HTML",
)
return
# Нельзя пожаловаться на самого себя
if reported_user.id == reporter.id:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на самого себя</b>",
parse_mode="HTML"
parse_mode="HTML",
)
return
# Нельзя пожаловаться на бота
if reported_user.is_bot:
await message.answer(
"⚠️ <b>Нельзя пожаловаться на бота</b>",
parse_mode="HTML"
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"
parse_mode="HTML",
)
return
# Извлекаем причину (опционально)
parts = message.text.split(maxsplit=1)
parts = (message.text or "").split(maxsplit=1)
reason = parts[1] if len(parts) > 1 else "Не указана"
# Генерируем ID репорта
report_id = generate_report_id()
# === ФОРМИРУЕМ СООБЩЕНИЕ РЕПОРТА ===
# thread/topic исходного сообщения (если репортят из топика)
original_message_thread_id = reported_message.message_thread_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>Chat ID:</b> <code>{message.chat.id}</code>\n"
if original_message_thread_id:
report_text += f"📌 <b>Topic ID:</b> <code>{original_message_thread_id}</code>\n"
report_text += "\n"
# Причина
report_text += f"📝 <b>Причина:</b> {reason}\n\n"
report_text += "📄 <b>Текст сообщения:</b>\n"
# Текст сообщения
report_text += f"📄 <b>Текст сообщения:</b>\n"
message_content = None
if reported_message.text:
truncated_text = truncate_text(reported_message.text, max_length=300)
report_text += f"<code>{truncated_text}</code>\n\n"
message_content = reported_message.text
elif reported_message.caption:
truncated_caption = truncate_text(reported_message.caption, max_length=300)
report_text += f"<code>{truncated_caption}</code>\n\n"
message_content = reported_message.caption
else:
content_type = reported_message.content_type
report_text += f"<i>[{content_type}]</i>\n\n"
report_text += f"<i>[{reported_message.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
report_id=report_id,
message_thread_id=original_message_thread_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()
)
report_chat_id = settings.REPORT_CHAT_ID
report_thread_id = settings.REPORT_THREAD_ID
# Нормализуем: 0 считаем как "без топика"
if report_thread_id == 0:
report_thread_id = None
if report_chat_id:
send_params = {
"chat_id": report_chat_id,
"text": report_text,
"parse_mode": "HTML",
"reply_markup": keyboard.as_markup(),
}
if report_thread_id is not None:
send_params["message_thread_id"] = report_thread_id # отправка в конкретный топик
await message.bot.send_message(**send_params)
else:
# Отправляем всем владельцам
sent_count = 0
for owner_id in settings.OWNER_ID:
try:
@@ -254,24 +234,38 @@ async def report_cmd(message: Message) -> None:
if sent_count == 0:
raise Exception("Не удалось отправить репорт ни одному владельцу")
# Подтверждение пользователю
await manager.log_report(
report_id=report_id,
reporter_id=reporter.id,
reporter_username=reporter.username,
reported_user_id=reported_user.id,
reported_username=reported_user.username,
chat_id=message.chat.id,
chat_title=chat_title,
message_id=reported_message.message_id,
message_thread_id=original_message_thread_id,
message_text=message_content,
reason=reason
)
await message.answer(
"✅ <b>Жалоба отправлена администраторам</b>\n\n"
"Спасибо за бдительность! Администраторы рассмотрят вашу жалобу.",
parse_mode="HTML"
parse_mode="HTML",
)
# Логирование
logger.info(
f"Репорт #{report_id}: {reporter.id}{reported_user.id} в чате {message.chat.id}",
f"Репорт #{report_id}: {reporter.id}{reported_user.id} в чате {message.chat.id}"
+ (f" (топик {original_message_thread_id})" if original_message_thread_id else ""),
log_type="REPORT"
)
except Exception as e:
logger.error(f"Ошибка отправки репорта: {e}", log_type="REPORT")
await message.answer(
"❌ <b>Ошибка отправки жалобы</b>\n\nПопробуйте позже или обратитесь к администратору напрямую.",
parse_mode="HTML"
"❌ <b>Ошибка отправки жалобы</b>\n\n"
"Попробуйте позже или обратитесь к администратору напрямую.",
parse_mode="HTML",
)
@@ -280,30 +274,28 @@ async def report_cmd(message: Message) -> None:
@router.callback_query(F.data.startswith("report:ban:"), IsAdmin())
async def report_ban_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Забанить'"""
manager = get_manager()
try:
# Парсим данные: report:ban:chat_id:user_id:report_id
parts = callback.data.split(":")
parts = (callback.data or "").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
await callback.bot.ban_chat_member(chat_id=chat_id, user_id=user_id)
await manager.repo.update_report_status(
report_id=report_id,
status="banned",
processed_by=callback.from_user.id
)
admin_name = format_user(callback.from_user)
updated_text = (callback.message.text if callback.message else "") + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
if callback.message:
await callback.message.edit_text(text=updated_text, parse_mode="HTML")
await callback.answer("✅ Пользователь забанен", show_alert=True)
@@ -323,30 +315,28 @@ async def report_ban_callback(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("report:delete:"), IsAdmin())
async def report_delete_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Удалить'"""
manager = get_manager()
try:
# Парсим данные: report:delete:chat_id:message_id:report_id
parts = callback.data.split(":")
parts = (callback.data or "").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
await callback.bot.delete_message(chat_id=chat_id, message_id=message_id)
await manager.repo.update_report_status(
report_id=report_id,
status="deleted",
processed_by=callback.from_user.id
)
admin_name = format_user(callback.from_user)
updated_text = (callback.message.text if callback.message else "") + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
# Обновляем сообщение
updated_text = callback.message.text + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
# Убираем кнопки
await callback.message.edit_text(
text=updated_text,
parse_mode="HTML"
)
if callback.message:
await callback.message.edit_text(text=updated_text, parse_mode="HTML")
await callback.answer("✅ Сообщение удалено", show_alert=True)
@@ -365,25 +355,28 @@ async def report_delete_callback(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("report:close:"), IsAdmin())
async def report_close_callback(callback: CallbackQuery) -> None:
"""Обрабатывает нажатие кнопки 'Закрыть'"""
"""Обрабатывает нажатие кнопки 'Закрыть' (и удаляет сообщение репорта)"""
manager = get_manager()
try:
# Парсим данные: report:close:report_id
parts = callback.data.split(":")
parts = (callback.data or "").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 manager.repo.update_report_status(
report_id=report_id,
status="closed",
processed_by=callback.from_user.id
)
await callback.answer("✅ Репорт закрыт")
# Удаляем сообщение с репортом в админ-чате/топике
if callback.message:
try:
await callback.message.delete()
except TelegramBadRequest as e:
logger.warning(f"Не удалось удалить сообщение репорта: {e}", log_type="REPORT")
logger.info(
f"Репорт #{report_id} закрыт админом {callback.from_user.id}",
log_type="REPORT"
@@ -394,15 +387,11 @@ async def report_close_callback(callback: CallbackQuery) -> None:
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"
@@ -425,23 +414,44 @@ async def report_help_cmd(message: Message) -> None:
await message.answer(text, parse_mode="HTML")
@router.message(Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
@router.message(
Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True),
IsAdmin()
)
async def report_stats_cmd(message: Message) -> None:
"""
Показывает статистику по репортам (для админов).
"""Показывает статистику по репортам (для админов)"""
manager = get_manager()
TODO: Реализовать сохранение статистики в БД
"""
text = (
"📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
"⚠️ <i>Функция в разработке</i>\n\n"
"Планируется:\n"
"Всего репортов за всё время\n"
"• Топ жалобщиков\n"
"• Топ нарушителей\n"
"• Распределение по причинам\n"
"• Статистика обработки\n\n"
"💡 <i>Для реализации нужно добавить таблицу reports в БД</i>"
)
stats = await manager.repo.get_report_stats()
top_reporters = await manager.repo.get_top_reporters(limit=5)
top_reported = await manager.repo.get_top_reported_users(limit=5)
if not stats:
await message.answer("❌ <b>Ошибка получения статистики</b>", parse_mode="HTML")
return
text = "📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
text += "📈 <b>Общая статистика:</b>\n"
text += f"├─ Всего репортов: <b>{stats.get('total', 0)}</b>\n"
text += f"├─ В ожидании: <b>{stats.get('pending', 0)}</b>\n"
text += f"├─ Закрыто: <b>{stats.get('closed', 0)}</b>\n"
text += f"├─ Забанено: <b>{stats.get('banned', 0)}</b>\n"
text += f"└─ Удалено: <b>{stats.get('deleted', 0)}</b>\n\n"
if top_reporters:
text += "👥 <b>Топ жалобщиков:</b>\n"
for i, (user_id, username, count) in enumerate(top_reporters, 1):
username_display = f"@{username}" if username and not username.startswith("id") else (username or f"id{user_id}")
text += f"{i}. {username_display} — <b>{count}</b> реп.\n"
text += "\n"
if top_reported:
text += "⚠️ <b>Топ нарушителей:</b>\n"
for i, (user_id, username, count) in enumerate(top_reported, 1):
username_display = f"@{username}" if username and not username.startswith("id") else (username or f"id{user_id}")
text += f"{i}. {username_display} — <b>{count}</b> жалоб\n"
text += "\n"
text += f"🕐 <b>Обновлено:</b> {format_datetime(datetime.now())}"
await message.answer(text, parse_mode="HTML")

View File

@@ -10,10 +10,12 @@ 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
from bot.utils import log_action, tg_emoji
__all__ = ("router",)
CMD: str = "start"
router: Router = Router(name="start_cmd_router")
def kb(text: str = "Создатель⬆️", url: str = "https://t.me/verdise"):
@@ -21,7 +23,6 @@ def kb(text: str = "Создатель⬆️", url: str = "https://t.me/verdise"
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)
@@ -36,6 +37,7 @@ async def start_cmd(update: Message | CallbackQuery) -> None:
update: Message или CallbackQuery
"""
print(123)
# Определяем тип update и извлекаем данные
if isinstance(update, CallbackQuery):
message = update.message
@@ -51,98 +53,89 @@ async def start_cmd(update: Message | CallbackQuery) -> None:
# Формируем текст помощи
help_text = (
"🤖 <b>PrimoGuard - Бот-модератор</b>\n\n"
"Автоматическое удаление сообщений с запрещёнными словами.\n"
"Поддержка подстрок, лемм, временных блокировок и режимов модерации.\n\n"
f'{tg_emoji(4961073056677103064)} <b>PrimoGuard - Бот-модератор</b>\n\n'
'<blockquote>Автоматическое удаление сообщений с запрещёнными словами.\nПоддержка подстрок, лемм, временных блокировок и режимов модерации.</blockquote>\n\n'
)
# === Команды просмотра ===
help_text += (
"📋 <b>Просмотр:</b>\n"
"/list — список всех правил и слов\n"
"/stats — статистика по удалениям\n"
"/id — получение айди пользователя\n"
"/chatid — получение айди чата\n\n"
f'{tg_emoji(4961141003059725568)} <b>Просмотр:</b>\n'
'<b>/list</b> — список всех правил и слов\n'
'<b>/stats</b> — статистика по удалениям\n'
'<b>/id</b> — получение айди пользователя\n'
'<b>/chatid</b> — получение айди чата\n\n'
)
# === Постоянные банворды ===
help_text += (
" <b>Добавить банворд (постоянно):</b>\n"
"/addword <code>слово</code> — подстрока (простой поиск)\n"
"/addlemma <code>слово</code> — лемма (все формы слова)\n"
"/addpart <code>комбинация</code> — часть (поиск без пробелов)\n\n"
f'{tg_emoji(4961019408240608234)} <b>Добавить банворд (постоянно):</b>\n'
'<code>/addword</code> <code>слово</code> — подстрока (простой поиск)\n'
'<code>/addlemma</code> <code>слово</code> — лемма (все формы слова)\n'
'<code>/addpart</code> <code>комбинация</code> — часть (поиск без пробелов)\n\n'
)
# === Временные банворды ===
help_text += (
" <b>Добавить банворд (временно):</b>\n"
"/addtempword <code>слово минуты</code> — временная подстрока\n"
"/addtemplemma <code>слово минуты</code> — временная лемма\n"
"<i>Пример: /addtempword спам 60</i>\n\n"
f'{tg_emoji(4960719190026618714)} <b>Добавить банворд (временно):</b>\n'
'<code>/addtempword</code> <code>слово минуты</code> — временная подстрока\n'
'<code>/addtemplemma</code> <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"
f'{tg_emoji(4963010134172239128)} <b>Исключения (whitelist):</b>\n'
'<code>/addexcept</code> <code>текст</code> — добавить исключение\n'
'<code>/remexcept</code> <code>текст</code> — удалить исключение\n'
'<i>Исключения не проверяются фильтром</i>\n\n'
)
# === Режимы модерации ===
help_text += (
"🔇 <b>Режим тишины:</b>\n"
"/silence <code>минуты</code> — удалять ВСЕ сообщения\n"
"/unsilence — отключить режим тишины\n"
"/report — отправить репорт\n\n"
f'{tg_emoji(4960987543878239236)} <b>Режим тишины:</b>\n'
'<code>/silence</code> <code>минуты</code> — удалять ВСЕ сообщения\n'
'<b>/unsilence</b> — отключить режим тишины\n'
'<code>/report</code> — отправить репорт\n\n'
)
help_text += (
"⚔️ <b>Режим антиконфликта:</b>\n"
"/addconflictword <code>слово</code> — добавить конфликтное слово\n"
"/addconflictlemma <code>слово</code> — добавить конфликтную лемму\n"
"/stopconflict <code>минуты</code> — активировать режим\n"
"/unstopconflict — отключить режим\n\n"
f'{tg_emoji(4960986152308835400)} <b>Режим антиконфликта:</b>\n'
'<code>/addconflictword</code> <code>слово</code> — добавить конфликтное слово\n'
'<code>/addconflictlemma</code> <code>слово</code> — добавить конфликтную лемму\n'
'<code>/stopconflict</code> <code>минуты</code> — активировать режим\n'
'<code>/unstopconflict</code> — отключить режим\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"
f'{tg_emoji(4961196485447254983)} <b>Удалить:</b>\n'
'<code>/remword</code> <code>слово</code> — удалить подстроку\n'
'<code>/remlemma</code> <code>слово</code> — удалить лемму\n'
'<code>/rempart</code> <code>комбинация</code> — удалить часть\n'
'<code>/remtempword</code> <code>слово</code> — удалить временную подстроку\n'
'<code>/remtemplemma</code> <code>слово</code> — удалить временную лемму\n'
'<code>/remconflictword</code> <code>слово</code> — удалить конфликтное слово\n'
'<code>/remconflictlemma</code> <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"
f'{tg_emoji(4960891456869893259)} <b>Управление админами (только для владельцев):</b>\n'
'<code>/addadmin</code> <i>ID</i> — добавить администратора\n'
'<code>/remadmin</code> <i>ID</i> — удалить администратора\n'
'<b>/redactcomment</b> — изменить комментарий под постом\n'
'<b>/listadmins</b> — список всех админов\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"
"💾 Все настройки сохраняются в базе данных"
f'{tg_emoji(4961021096162755737)} <b>Типы проверок:</b>\n'
'• <b>Подстрока</b> — простой поиск в тексте\n'
'• <b>Лемма</b> — все формы слова (купить→куплю, купил, купишь...)\n'
'• <b>Часть</b> — поиск без пробелов (обходит \"к у п и т ь\")\n'
'• <b>Временные</b> — автоматически удаляются через N минут\n'
'• <b>Конфликтные</b> — работают только в режиме /stopconflict\n\n'
)
# Отправляем ответ
@@ -166,4 +159,4 @@ async def start_cmd(update: Message | CallbackQuery) -> None:
log_type="ERROR"
)
if is_callback:
await update.answer(" Ошибка отображения справки", show_alert=True)
await update.answer(f'{tg_emoji(4963277744994518278)} Ошибка отображения справки', show_alert=True)