v1.2.0
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
330
bot/handlers/commands/users/bot_settings.py
Normal file
330
bot/handlers/commands/users/bot_settings.py
Normal 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("✅ Настройки отменены")
|
||||
48
bot/handlers/commands/users/cancel.py
Normal file
48
bot/handlers/commands/users/cancel.py
Normal 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("✅ Действие отменено")
|
||||
@@ -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("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
|
||||
|
||||
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><tg-emoji emoji-id=\"ID\">fallback</tg-emoji></code>\n\n"
|
||||
"📌 <b>Пример использования в коде:</b>\n"
|
||||
"<code>text = 'Привет <tg-emoji emoji-id=\"5368324170671202286\">👍</tg-emoji>'\n"
|
||||
"await message.answer(text, parse_mode=\"HTML\")</code>\n\n"
|
||||
'<code><tg-emoji emoji-id="ID">fallback</tg-emoji></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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user