Первый коммит
This commit is contained in:
16
bot/handlers/commands/__init__.py
Normal file
16
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from aiogram import Router
|
||||
|
||||
#from .admins import router as admin_cmd_router
|
||||
from .users import router as users_cmd_router
|
||||
#from .settings import router as settings_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
#settings_cmd_router,
|
||||
#admin_cmd_router,
|
||||
users_cmd_router,
|
||||
)
|
||||
18
bot/handlers/commands/admins/__init__.py
Normal file
18
bot/handlers/commands/admins/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from aiogram import Router
|
||||
|
||||
#from .ban_cmd import router as ban_cmd_router
|
||||
from .all_cmd import router as all_cmd_router
|
||||
from .pin_cmd import router as pin_cmd_router
|
||||
from .kick_cmd import router as kick_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
router.include_routers(
|
||||
#ban_cmd_router,
|
||||
kick_cmd_router,
|
||||
pin_cmd_router,
|
||||
all_cmd_router,
|
||||
|
||||
)
|
||||
81
bot/handlers/commands/admins/all_cmd.py
Normal file
81
bot/handlers/commands/admins/all_cmd.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from asyncio import create_task
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from bot.core.bots import bot, BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.utils import status_clear, auto_delete_message, hidden_admins_message
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
# Ключ для команды
|
||||
CMD: str = "all"
|
||||
# Инициализация роутера
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(
|
||||
F.text.lower().regexp(rf"^({'|'.join(COMMANDS[CMD])})\s?.*"), # ловим текст без префикса
|
||||
F.chat.type.in_({"supergroup", "group"}),
|
||||
IsOwner()
|
||||
)
|
||||
@router.message(
|
||||
Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True),
|
||||
F.chat.type.in_({"supergroup", "group"}),
|
||||
IsOwner()
|
||||
)
|
||||
async def notify_all_text(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /all, /call и текстовых эквивалентов типа "Калл Привет всем".
|
||||
|
||||
Функционал:
|
||||
1. Считывает весь текст после команды.
|
||||
2. Формирует скрытое сообщение для администраторов.
|
||||
3. Отправляет сообщение в чат.
|
||||
4. Автоматически удаляет сообщение через неделю.
|
||||
5. Пытается закрепить сообщение в чате.
|
||||
|
||||
Args:
|
||||
message (Message): Объект входящего сообщения.
|
||||
state (FSMContext): Контекст FSM, используется для очистки состояния.
|
||||
"""
|
||||
# Очистка состояния FSM перед выполнением команды
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
# Извлечение текста после команды
|
||||
parts: list[str] = message.text.split(" ", 1)
|
||||
custom_text: str = parts[1] if len(parts) > 1 else "⚡ Внимание всем!"
|
||||
|
||||
# Формирование скрытого текста для администраторов
|
||||
hidden_text: str = await hidden_admins_message(message=message, text=custom_text)
|
||||
|
||||
# Отправка сообщения в чат
|
||||
sent_message: Message = await message.answer(hidden_text)
|
||||
|
||||
# Запуск асинхронной задачи по удалению сообщения через 7 дней
|
||||
create_task(
|
||||
auto_delete_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
delay=604800 # 7 дней в секундах
|
||||
)
|
||||
)
|
||||
|
||||
# Попытка закрепить сообщение и удалить "системное" сообщение о закреплении
|
||||
try:
|
||||
await bot.pin_chat_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=sent_message.message_id,
|
||||
disable_notification=False
|
||||
)
|
||||
# Иногда Telegram создает дополнительное уведомление при закреплении
|
||||
await bot.delete_message(chat_id=message.chat.id, message_id=sent_message.message_id + 1)
|
||||
logger.debug(f"[ALL] Сообщение закреплено: {custom_text}")
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"[ALL] Ошибка закрепления сообщения: {e}")
|
||||
258
bot/handlers/commands/admins/ban_cmd.py
Normal file
258
bot/handlers/commands/admins/ban_cmd.py
Normal file
@@ -0,0 +1,258 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, User
|
||||
from html import escape
|
||||
|
||||
from bot.filters import IsAdmin
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
from database import db
|
||||
|
||||
# Настройки роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
from middleware import logger
|
||||
|
||||
CMD: str = "ban"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS[CMD], ignore_case=True), IsAdmin())
|
||||
async def ban_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /ban для блокировки пользователей.
|
||||
Использование: /ban <user_id> или ответ на сообщение пользователя + /ban
|
||||
"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
try:
|
||||
# Проверяем есть ли ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
# Бан по ответу на сообщение
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
if not target_user:
|
||||
await message.answer("❌ Не удалось определить пользователя")
|
||||
return
|
||||
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
|
||||
# Проверяем, не пытаемся ли забанить бота
|
||||
if target_user_id == message.bot.id:
|
||||
await message.answer("❌ Нельзя заблокировать бота!")
|
||||
return
|
||||
|
||||
# Баним пользователя
|
||||
success: bool = await _ban_user(target_user_id, target_username, message)
|
||||
|
||||
if success:
|
||||
safe_username: str = escape(target_username)
|
||||
response_text = f"✅ Пользователь {safe_username} (ID: {target_user_id}) заблокирован!"
|
||||
|
||||
# Пытаемся забанить в чате (если команда вызвана в группе/чате)
|
||||
if message.chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
await message.bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user_id
|
||||
)
|
||||
response_text += "\n🚫 Пользователь исключен из чата."
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось исключить пользователя из чата: {e}")
|
||||
response_text += "\n⚠️ Не удалось исключить пользователя из чата."
|
||||
|
||||
await message.answer(
|
||||
text=response_text,
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Не удалось заблокировать пользователя")
|
||||
|
||||
else:
|
||||
# Бан по ID пользователя
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /ban\n"
|
||||
"• Или укажите ID: /ban <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
|
||||
# Проверяем, не пытаемся ли забанить бота
|
||||
if target_user_id == message.bot.id:
|
||||
await message.answer("❌ Нельзя заблокировать бота!")
|
||||
return
|
||||
|
||||
success: bool = await _ban_user(target_user_id, f"ID{target_user_id}", message)
|
||||
|
||||
if success:
|
||||
response_text = f"✅ Пользователь (ID: {target_user_id}) заблокирован!"
|
||||
|
||||
# Пытаемся забанить в чате
|
||||
if message.chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
await message.bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user_id
|
||||
)
|
||||
response_text += "\n🚫 Пользователь исключен из чата."
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось исключить пользователя из чата: {e}")
|
||||
response_text += "\n⚠️ Не удалось исключить пользователя из чата."
|
||||
|
||||
await message.answer(
|
||||
text=response_text,
|
||||
parse_mode=None
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Пользователь не найден или уже заблокирован")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка в команде /ban: {e}")
|
||||
await message.answer(
|
||||
"⚠️ Произошла непредвиденная ошибка при выполнении команды.\n"
|
||||
"Попробуйте повторить действие позже или нажмите /start"
|
||||
)
|
||||
|
||||
|
||||
async def _ban_user(user_id: int, username: str, message: Message) -> bool:
|
||||
"""
|
||||
Внутренняя функция для блокировки пользователя.
|
||||
"""
|
||||
try:
|
||||
# Сначала проверяем существует ли пользователь
|
||||
user: User | None = await db.get_user(user_id)
|
||||
|
||||
if not user:
|
||||
# Если пользователя нет - создаем его забаненным
|
||||
await db.add_user(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
full_name=username
|
||||
)
|
||||
|
||||
# Баним пользователя
|
||||
await db.ban_user(user_id)
|
||||
|
||||
# Логируем действие
|
||||
admin_username = message.from_user.username or message.from_user.full_name or f"ID{message.from_user.id}"
|
||||
logger.info(f"🛑 Админ @{admin_username} заблокировал пользователя @{username} (ID: {user_id})")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при блокировке пользователя {user_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("unban", ignore_case=True), IsAdmin())
|
||||
async def unban_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /unban для разблокировки пользователей.
|
||||
"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
try:
|
||||
if message.reply_to_message:
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
if not target_user:
|
||||
await message.answer("❌ Не удалось определить пользователя")
|
||||
return
|
||||
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
else:
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /unban\n"
|
||||
"• Или укажите ID: /unban <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
target_username: str = f"ID{target_user_id}"
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
return
|
||||
|
||||
# Разбаниваем пользователя
|
||||
await db.unban_user(target_user_id)
|
||||
|
||||
# Логируем действие
|
||||
admin_username: str = message.from_user.username or message.from_user.full_name or f"ID{message.from_user.id}"
|
||||
logger.info(f"🔓 Админ @{admin_username} разблокировал пользователя @{target_username} (ID: {target_user_id})")
|
||||
|
||||
# Экранируем специальные символы
|
||||
safe_username: str = escape(target_username)
|
||||
|
||||
response_text = f"✅ Пользователь {safe_username} (ID: {target_user_id}) разблокирован!"
|
||||
|
||||
# Пытаемся разбанить в чате
|
||||
if message.chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
await message.bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=target_user_id
|
||||
)
|
||||
response_text += "\n👥 Пользователь может вернуться в чат."
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось разблокировать пользователя в чате: {e}")
|
||||
|
||||
await message.answer(
|
||||
text=response_text,
|
||||
parse_mode=None
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при разблокировке пользователя: {e}")
|
||||
await message.answer("❌ Не удалось разблокировать пользователя")
|
||||
|
||||
|
||||
@router.message(Command("banned_list", ignore_case=True), IsAdmin())
|
||||
async def banned_list_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /banned_list для просмотра списка забаненных пользователей.
|
||||
"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
try:
|
||||
# Получаем всех пользователей включая забаненных
|
||||
all_users: list[User] = await db.get_all_users(include_banned=True)
|
||||
|
||||
# Фильтруем только забаненных
|
||||
banned_users: list[User] = [user for user in all_users if getattr(user, 'status', None) == "banned"]
|
||||
|
||||
if not banned_users:
|
||||
await message.answer("📭 Список забаненных пользователей пуст")
|
||||
return
|
||||
|
||||
# Формируем сообщение со списком
|
||||
banned_list: str = "🚫 Заблокированные пользователи:\n\n"
|
||||
|
||||
for user in banned_users[:50]: # Ограничиваем вывод
|
||||
username: str = f"@{user.username}" if getattr(user, 'username', None) else getattr(user, 'full_name',
|
||||
'Неизвестно')
|
||||
# Экранируем специальные символы
|
||||
safe_username = escape(username)
|
||||
user_id = getattr(user, 'id', 'N/A')
|
||||
banned_list += f"• {safe_username} (ID: {user_id})\n"
|
||||
|
||||
if len(banned_users) > 50:
|
||||
banned_list += f"\n... и еще {len(banned_users) - 50} пользователей"
|
||||
|
||||
await message.answer(banned_list, parse_mode=None)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при получении списка забаненных: {e}")
|
||||
await message.answer("❌ Не удалось получить список забаненных пользователей")
|
||||
277
bot/handlers/commands/admins/kick_cmd.py
Normal file
277
bot/handlers/commands/admins/kick_cmd.py
Normal file
@@ -0,0 +1,277 @@
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, User
|
||||
from html import escape
|
||||
|
||||
from bot import bot
|
||||
from bot.filters import IsAdmin
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
|
||||
# Настройки роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
from middleware import logger
|
||||
|
||||
CMD: str = "kick"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS[CMD], ignore_case=True), IsAdmin())
|
||||
async def kick_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /kick для кика пользователей из чата.
|
||||
Использование: /kick <user_id> или ответ на сообщение пользователя + /kick
|
||||
"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
# Проверяем, что команда используется в группе/супергруппе
|
||||
if message.chat.type not in ["group", "supergroup"]:
|
||||
await message.answer("❌ Эта команда работает только в группах и супергруппах!")
|
||||
return
|
||||
|
||||
# Проверяем есть ли ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
# Кик по ответу на сообщение
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
|
||||
# Кикаем пользователя
|
||||
success: bool = await _kick_user(target_user_id, target_username, message)
|
||||
|
||||
if success:
|
||||
safe_username: str = escape(target_username)
|
||||
await message.answer(
|
||||
text=f"👢 Пользователь {safe_username} (ID: {target_user_id}) кикнут из чата!",
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Не удалось кикнуть пользователя")
|
||||
|
||||
else:
|
||||
# Кик по ID пользователя
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /kick\n"
|
||||
"• Или укажите ID: /kick <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
success: bool = await _kick_user(target_user_id, f"ID{target_user_id}", message)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
text=f"👢 Пользователь (ID: {target_user_id}) кикнут из чата!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Пользователь не найден или не удалось кикнуть")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
|
||||
|
||||
async def _kick_user(user_id: int, username: str, message: Message) -> bool:
|
||||
"""
|
||||
Внутренняя функция для кика пользователя из чата.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя для кика
|
||||
username: Имя пользователя для логов
|
||||
message: Объект сообщения для контекста
|
||||
|
||||
Returns:
|
||||
bool: Успешно ли кикнут пользователь
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что бот имеет права администратора в чате
|
||||
bot_member = await bot.get_chat_member(message.chat.id, bot.id)
|
||||
if not bot_member.can_restrict_members:
|
||||
await message.answer("❌ У меня нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Проверяем, что целевой пользователь не является администратором/владельцем
|
||||
target_member = await bot.get_chat_member(message.chat.id, user_id)
|
||||
if target_member.status in ["creator", "administrator"]:
|
||||
await message.answer("❌ Нельзя кикнуть администратора или создателя чата!")
|
||||
return False
|
||||
|
||||
# Проверяем, что отправитель команды имеет права администратора
|
||||
admin_member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
if admin_member.status not in ["creator", "administrator"]:
|
||||
await message.answer("❌ У вас нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Кикаем пользователя из чата
|
||||
await bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id,
|
||||
revoke_messages=False # Не удаляем сообщения пользователя
|
||||
)
|
||||
|
||||
# Сразу разбаниваем, чтобы пользователь мог вернуться по приглашению
|
||||
await bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Логируем действие
|
||||
admin_username = message.from_user.username or message.from_user.full_name
|
||||
logger.info(
|
||||
f"👢 Админ @{admin_username} кикнул пользователя @{username} (ID: {user_id}) из чата {message.chat.title}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при кике пользователя {user_id}: {e}")
|
||||
await message.answer(f"❌ Ошибка при кике пользователя: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("kick_ban", ignore_case=True), IsAdmin())
|
||||
async def kick_ban_user_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /kick_ban для кика пользователя с удалением сообщений.
|
||||
Использование: /kick_ban <user_id> или ответ на сообщение пользователя + /kick_ban
|
||||
"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
# Проверяем, что команда используется в группе/супергруппе
|
||||
if message.chat.type not in ["group", "supergroup"]:
|
||||
await message.answer("❌ Эта команда работает только в группах и супергруппах!")
|
||||
return
|
||||
|
||||
# Проверяем есть ли ответ на сообщение
|
||||
if message.reply_to_message:
|
||||
# Кик по ответу на сообщение
|
||||
target_user: User | None = message.reply_to_message.from_user
|
||||
target_user_id: int = target_user.id
|
||||
target_username: str = target_user.username or target_user.full_name or f"ID{target_user_id}"
|
||||
|
||||
# Кикаем пользователя с удалением сообщений
|
||||
success: bool = await _kick_ban_user(target_user_id, target_username, message)
|
||||
|
||||
if success:
|
||||
safe_username: str = escape(target_username)
|
||||
await message.answer(
|
||||
text=f"💥 Пользователь {safe_username} (ID: {target_user_id}) кикнут с удалением сообщений!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Не удалось кикнуть пользователя")
|
||||
|
||||
else:
|
||||
# Кик по ID пользователя
|
||||
command_parts: list[str] = message.text.split()
|
||||
if len(command_parts) < 2:
|
||||
await message.answer(
|
||||
"ℹ️ Использование команды:\n"
|
||||
"• Ответьте на сообщение пользователя командой /kick_ban\n"
|
||||
"• Или укажите ID: /kick_ban <user_id>"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
target_user_id: int = int(command_parts[1])
|
||||
success: bool = await _kick_ban_user(target_user_id, f"ID{target_user_id}", message)
|
||||
|
||||
if success:
|
||||
await message.answer(
|
||||
text=f"💥 Пользователь (ID: {target_user_id}) кикнут с удалением сообщений!",
|
||||
parse_mode=None # Отключаем разметку
|
||||
)
|
||||
else:
|
||||
await message.answer("❌ Пользователь не найден или не удалось кикнуть")
|
||||
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат ID пользователя")
|
||||
|
||||
|
||||
async def _kick_ban_user(user_id: int, username: str, message: Message) -> bool:
|
||||
"""
|
||||
Внутренняя функция для кика пользователя с удалением сообщений.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя для кика
|
||||
username: Имя пользователя для логов
|
||||
message: Объект сообщения для контекста
|
||||
|
||||
Returns:
|
||||
bool: Успешно ли кикнут пользователь
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что бот имеет права администратора в чате
|
||||
bot_member = await bot.get_chat_member(message.chat.id, bot.id)
|
||||
if not bot_member.can_restrict_members:
|
||||
await message.answer("❌ У меня нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Проверяем, что целевой пользователь не является администратором/владельцем
|
||||
target_member = await bot.get_chat_member(message.chat.id, user_id)
|
||||
if target_member.status in ["creator", "administrator"]:
|
||||
await message.answer("❌ Нельзя кикнуть администратора или создателя чата!")
|
||||
return False
|
||||
|
||||
# Проверяем, что отправитель команды имеет права администратора
|
||||
admin_member = await bot.get_chat_member(message.chat.id, message.from_user.id)
|
||||
if admin_member.status not in ["creator", "administrator"]:
|
||||
await message.answer("❌ У вас нет прав для кика пользователей!")
|
||||
return False
|
||||
|
||||
# Кикаем пользователя из чата с удалением сообщений
|
||||
await bot.ban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id,
|
||||
revoke_messages=True # Удаляем сообщения пользователя
|
||||
)
|
||||
|
||||
# Сразу разбаниваем, чтобы пользователь мог вернуться по приглашению
|
||||
await bot.unban_chat_member(
|
||||
chat_id=message.chat.id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Логируем действие
|
||||
admin_username = message.from_user.username or message.from_user.full_name
|
||||
logger.info(
|
||||
f"💥 Админ @{admin_username} кикнул пользователя @{username} (ID: {user_id}) из чата {message.chat.title} с удалением сообщений")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при кике пользователя {user_id} с удалением сообщений: {e}")
|
||||
await message.answer(f"❌ Ошибка при кике пользователя: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@router.message(Command("kick_list", ignore_case=True), IsAdmin())
|
||||
async def kick_help_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Команда /kick_list для показа справки по командам кика.
|
||||
"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
help_text = """
|
||||
🤖 **Команды модерации:**
|
||||
|
||||
**👢 /kick** - Кикнуть пользователя (может вернуться по приглашению)
|
||||
• Ответьте на сообщение пользователя с командой /kick
|
||||
• Или используйте: /kick <user_id>
|
||||
|
||||
**💥 /kick_ban** - Кикнуть пользователя с удалением сообщений
|
||||
• Ответьте на сообщение пользователя с командой /kick_ban
|
||||
• Или используйте: /kick_ban <user_id>
|
||||
|
||||
**🚫 /ban** - Полностью забанить пользователя
|
||||
**🔓 /unban** - Разбанить пользователя
|
||||
**📋 /banned_list** - Список забаненных
|
||||
|
||||
⚠️ *Команды работают только в группах и требуют прав администратора*
|
||||
"""
|
||||
|
||||
await message.answer(help_text, parse_mode=None)
|
||||
77
bot/handlers/commands/admins/pin_cmd.py
Normal file
77
bot/handlers/commands/admins/pin_cmd.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from asyncio import create_task
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from bot.core.bots import BotInfo, bot
|
||||
from bot.filters import IsOwner
|
||||
from bot.templates import msg
|
||||
from bot.utils import status_clear
|
||||
from bot.utils.auto_delete import auto_delete_message
|
||||
from configs import COMMANDS
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "pin".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def pin_cmd(message: Message, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /pin для закрепления последнего сообщения или ответа.
|
||||
"""
|
||||
# Если есть reply → закрепляем его, иначе закрепляем предыдущее сообщение
|
||||
if message.reply_to_message:
|
||||
target_message_id = message.reply_to_message.message_id
|
||||
else:
|
||||
# Закрепляем предыдущее сообщение (команда - 1)
|
||||
target_message_id = message.message_id - 1
|
||||
|
||||
try:
|
||||
await bot.pin_chat_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=target_message_id,
|
||||
disable_notification=False
|
||||
)
|
||||
|
||||
# Автоудаление через 7 суток (удаляем закрепленное сообщение)
|
||||
create_task(
|
||||
auto_delete_message(
|
||||
chat_id=message.chat.id,
|
||||
message_id=target_message_id,
|
||||
delay=604800
|
||||
)
|
||||
)
|
||||
|
||||
await msg(update=message, text="✅ Сообщение успешно закреплено", state=state)
|
||||
|
||||
except Exception as e:
|
||||
await msg(update=message, text=f"❌ Ошибка закрепления: {e}", state=state)
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold().isin(COMMANDS[CMD]), IsOwner())
|
||||
async def pin_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик кнопки с callback_data="pin".
|
||||
"""
|
||||
await status_clear(update=callback.message, state=state)
|
||||
|
||||
try:
|
||||
await bot.pin_chat_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id,
|
||||
disable_notification=False
|
||||
)
|
||||
|
||||
create_task(
|
||||
auto_delete_message(
|
||||
chat_id=callback.message.chat.id,
|
||||
message_id=callback.message.message_id,
|
||||
delay=604800
|
||||
)
|
||||
)
|
||||
|
||||
await callback.answer("✅ Сообщение закреплено")
|
||||
|
||||
except Exception as e:
|
||||
await callback.answer(f"❌ Ошибка: {e}", show_alert=True)
|
||||
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
51
bot/handlers/commands/admins/settings_cmd.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils.interesting_facts import interesting_fact
|
||||
from bot.core.bots import BotInfo
|
||||
from configs import COMMANDS, RpValue
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
CMD: str = "settings".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
async def start_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /start"""
|
||||
await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Инфо-канал🗂", url=CustomConfig.INFO_URL))
|
||||
ikb.row(InlineKeyboardButton(text="Вступление🚀", callback_data='new'),
|
||||
InlineKeyboardButton(text="Анкета📖", callback_data='anketa'))
|
||||
ikb.row(InlineKeyboardButton(text="Связь с администрацией🌐", callback_data='admin'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||
|
||||
Я ваш искусственный помощник по ролевой - <b>{rp_name}</b>!
|
||||
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||
|
||||
Интересный факт:
|
||||
<blockquote>{fact}</blockquote>
|
||||
"""
|
||||
).format(
|
||||
url=message.from_user.url if message.from_user else "",
|
||||
name=message.from_user.first_name if message.from_user else "пользователь",
|
||||
rp_name=RpValue.RP_NAME,
|
||||
fact=interesting_fact(),
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(update=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||
19
bot/handlers/commands/settings/__init__.py
Normal file
19
bot/handlers/commands/settings/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .set_description_cmd import router as set_description_cmd_router
|
||||
from .set_name_cmd import router as set_name_cmd_router
|
||||
from .set_widget_cmd import router as set_widget_cmd_router
|
||||
from .settings_cmd import router as settings_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
settings_cmd_router,
|
||||
set_name_cmd_router,
|
||||
set_description_cmd_router,
|
||||
set_widget_cmd_router,
|
||||
)
|
||||
173
bot/handlers/commands/settings/set_description_cmd.py
Normal file
173
bot/handlers/commands/settings/set_description_cmd.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
|
||||
from bot.templates import msg
|
||||
from bot.utils import format_retry_time, status_clear
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
# Название команды
|
||||
CMD: str = "set_description".lower()
|
||||
|
||||
# Роутер для обработки команды /set_description
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
class SetBotDescriptionForm(StatesGroup):
|
||||
"""Состояния FSM для изменения короткого описания бота."""
|
||||
new_description: State = State()
|
||||
|
||||
|
||||
async def handle_set_bot_description(
|
||||
description: str,
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Установка короткого описания (short description) бота с обработкой FSM и ошибок API.
|
||||
|
||||
Args:
|
||||
description (str): Новый текст описания (до 120 символов).
|
||||
message (Message | CallbackQuery): Сообщение или callback-запрос.
|
||||
state (FSMContext): Контекст FSM.
|
||||
bot (Bot): Экземпляр бота.
|
||||
"""
|
||||
# Проверка ограничения Telegram
|
||||
if len(description) > 120:
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Короткое описание бота должно быть не более 120 символов. Текущая длина: {length}").format(
|
||||
length=len(description)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Установка нового короткого описания
|
||||
await bot.set_my_short_description(short_description=description)
|
||||
|
||||
# Сохраняем текущее значение в BotInfo
|
||||
BotInfo.short_description = description
|
||||
|
||||
# Сбрасываем состояние FSM
|
||||
await state.clear()
|
||||
|
||||
# Отправляем сообщение об успехе
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("✅ Короткое описание бота успешно изменено на: <b>{description}</b>").format(
|
||||
description=description
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
logger.info(f"Короткое описание бота изменено на: {description}")
|
||||
|
||||
except TelegramRetryAfter as e:
|
||||
retry_text: str = format_retry_time(e.retry_after)
|
||||
logger.warning(f"Превышен лимит запросов при смене short description. Попробуйте через {retry_text}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("⚠️ Слишком частая смена короткого описания!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
logger.error(f"Ошибка Telegram API при изменении короткого описания: {e}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Ошибка Telegram API при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при изменении короткого описания: {e}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Непредвиденная ошибка при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot,
|
||||
command: CommandObject | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Обработчик команды /set_description для короткого описания.
|
||||
|
||||
Поддерживает:
|
||||
1. Немедленное изменение через аргумент (/set_description TEXT).
|
||||
2. Callback-запрос.
|
||||
3. FSM-ввод.
|
||||
"""
|
||||
current_description: str = BotInfo.description
|
||||
|
||||
# Вариант 1: если пользователь передал аргумент к команде
|
||||
if command and command.args:
|
||||
description: str = command.args.strip()
|
||||
if len(description) > 120:
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Короткое описание не должно превышать 120 символов. Текущая длина: {length}").format(
|
||||
length=len(description)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
return
|
||||
|
||||
await handle_set_bot_description(description, message, state, bot)
|
||||
return
|
||||
|
||||
# Вариант 2: без аргумента → включаем FSM
|
||||
await status_clear(update=message, state=state)
|
||||
text: str = _(
|
||||
"📝 <b>Смена короткого описания бота</b>\n\n"
|
||||
"Текущее короткое описание: <i>{current}</i>\n\n"
|
||||
"Введите новое короткое описание (максимум 120 символов):"
|
||||
).format(current=current_description)
|
||||
|
||||
await msg(update=message, text=text, markup=settings_keyboard(), state=state)
|
||||
await state.set_state(SetBotDescriptionForm.new_description)
|
||||
|
||||
|
||||
@router.message(SetBotDescriptionForm.new_description, IsOwner())
|
||||
async def process_new_bot_description(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Обработка ввода нового короткого описания через FSM.
|
||||
"""
|
||||
description: str = message.text.strip()
|
||||
|
||||
if not description:
|
||||
await message.answer(_("❌ Пожалуйста, введите корректное короткое описание."))
|
||||
return
|
||||
|
||||
await handle_set_bot_description(description, message, state, bot)
|
||||
157
bot/handlers/commands/settings/set_name_cmd.py
Normal file
157
bot/handlers/commands/settings/set_name_cmd.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
|
||||
from bot.templates import msg
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "set_name".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
class SetNameForm(StatesGroup):
|
||||
new_name: State = State()
|
||||
|
||||
|
||||
def format_retry_time(retry_after: int) -> str:
|
||||
"""Форматирование времени повторной попытки в читаемом виде"""
|
||||
hours, remainder = divmod(retry_after, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours} часов, {minutes} минут, {seconds} секунд"
|
||||
elif minutes > 0:
|
||||
return f"{minutes} минут, {seconds} секунд"
|
||||
else:
|
||||
return f"{seconds} секунд"
|
||||
|
||||
|
||||
async def handle_set_name(
|
||||
new_name: str,
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Установка имени бота с проверкой длины, обработкой перегрузки и логированием
|
||||
"""
|
||||
if len(new_name) > 64:
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Имя бота должно быть не более 64 символов. Текущая длина: {length}").format(
|
||||
length=len(new_name)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.set_my_name(new_name)
|
||||
BotInfo.first_name = new_name
|
||||
await state.clear()
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("✅ Имя бота успешно изменено на: <b>{new_name}</b>").format(new_name=new_name),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
logger.info(f"Имя бота изменено на: {new_name}")
|
||||
|
||||
except TelegramRetryAfter as e:
|
||||
retry_text: str = format_retry_time(e.retry_after)
|
||||
logger.warning(f"Превышен контроль перегрузки при смене имени. Попробуйте через {retry_text}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("⚠️ Слишком частая смена имени!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
logger.error(f"Ошибка Telegram API при изменении имени: {e}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Ошибка Telegram API: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при изменении имени: {e}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Непредвиденная ошибка: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot,
|
||||
command: CommandObject | None = None
|
||||
):
|
||||
"""
|
||||
Обработчик команды /set_name с поддержкой:
|
||||
1. Immediate установки через аргумент команды
|
||||
2. Callback query
|
||||
3. FSM ввод
|
||||
"""
|
||||
current_name = getattr(BotInfo, "first_name", "") or _("Не установлено")
|
||||
|
||||
# Immediate установка через аргумент команды
|
||||
if command and command.args:
|
||||
new_name = command.args.strip()
|
||||
if len(new_name) > 64:
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Имя не должно превышать 64 символа. Текущая длина: {length}").format(
|
||||
length=len(new_name)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
return
|
||||
await handle_set_name(new_name, message, state, bot)
|
||||
return
|
||||
|
||||
# Для callback query или пустой команды — показываем текущее имя и запускаем FSM
|
||||
await state.clear()
|
||||
if isinstance(message, CallbackQuery):
|
||||
await message.answer()
|
||||
text: str = _(
|
||||
"🤖 <b>Смена имени бота</b>\n\n"
|
||||
"Текущее имя: <i>{current}</i>\n\n"
|
||||
"Пожалуйста, введите новое имя для бота (максимум 64 символа):"
|
||||
).format(current=current_name)
|
||||
await msg(update=message, text=text, markup=settings_keyboard(), state=state)
|
||||
await state.set_state(SetNameForm.new_name)
|
||||
|
||||
|
||||
@router.message(SetNameForm.new_name, IsOwner())
|
||||
async def process_new_name(message: Message, state: FSMContext, bot: Bot):
|
||||
"""
|
||||
Обработка ввода нового имени через FSM
|
||||
"""
|
||||
new_name: str = message.text.strip()
|
||||
|
||||
if not new_name:
|
||||
await message.answer(_("❌ Пожалуйста, введите корректное имя."))
|
||||
return
|
||||
|
||||
await handle_set_name(new_name, message, state, bot)
|
||||
174
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
174
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from aiogram import Router, F, Bot
|
||||
from aiogram.exceptions import TelegramAPIError, TelegramRetryAfter
|
||||
from aiogram.filters import Command, CommandObject
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import StatesGroup, State
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.handlers.commands.settings.settings_cmd import settings_keyboard
|
||||
from bot.templates import msg
|
||||
from bot.utils import format_retry_time, status_clear
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "set_widget".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
class SetWidgetForm(StatesGroup):
|
||||
"""Состояния FSM для изменения виджета (описания бота)."""
|
||||
new_widget: State = State()
|
||||
|
||||
|
||||
async def handle_set_widget(
|
||||
new_widget: str,
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Устанавливает новое значение виджета (описания бота).
|
||||
|
||||
Args:
|
||||
new_widget (str): Новый текст виджета.
|
||||
message (Message | CallbackQuery): Объект сообщения или callback-запроса.
|
||||
state (FSMContext): Контекст состояния FSM.
|
||||
bot (Bot): Экземпляр текущего бота.
|
||||
"""
|
||||
# Проверка длины текста (Telegram API ограничивает description до 512 символов)
|
||||
if len(new_widget) > 512:
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Виджет бота должен быть не более 512 символов. Текущая длина: {length}").format(
|
||||
length=len(new_widget)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Устанавливаем описание через Telegram API
|
||||
await bot.set_my_description(description=new_widget)
|
||||
|
||||
# Сохраняем в BotInfo для локального использования
|
||||
BotInfo.widget = new_widget
|
||||
|
||||
# Очищаем состояние FSM
|
||||
await state.clear()
|
||||
|
||||
# Отправляем уведомление пользователю
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("✅ Виджет бота успешно изменён на: <b>{new_widget}</b>").format(
|
||||
new_widget=new_widget
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
logger.info(f"Виджет бота изменён на: {new_widget}")
|
||||
|
||||
except TelegramRetryAfter as e:
|
||||
# Если запрос слишком частый
|
||||
retry_text: str = format_retry_time(e.retry_after)
|
||||
logger.warning(f"Превышен лимит запросов при смене виджета. Попробуйте через {retry_text}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("⚠️ Слишком частая смена виджета!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
# Ошибка Telegram API
|
||||
logger.error(f"Ошибка Telegram API при изменении виджета: {e}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Ошибка Telegram API при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Непредвиденная ошибка
|
||||
logger.error(f"Непредвиденная ошибка при изменении виджета: {e}")
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Непредвиденная ошибка при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(
|
||||
message: Message | CallbackQuery,
|
||||
state: FSMContext,
|
||||
bot: Bot,
|
||||
command: CommandObject | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Обработчик команды /set_widget.
|
||||
|
||||
Поддерживает:
|
||||
1. Немедленное изменение через аргумент команды (/set_widget TEXT).
|
||||
2. Callback-запрос.
|
||||
3. FSM ввод.
|
||||
"""
|
||||
# Получаем текущее значение виджета
|
||||
current_widget: str = BotInfo.short_description
|
||||
|
||||
# Вариант 1: пользователь ввёл аргумент сразу (/set_widget TEXT)
|
||||
if command and command.args:
|
||||
new_widget: str = command.args.strip()
|
||||
if len(new_widget) > 512:
|
||||
await msg(
|
||||
update=message,
|
||||
text=_("❌ Виджет не должен превышать 512 символов. Текущая длина: {length}").format(
|
||||
length=len(new_widget)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
state=state
|
||||
)
|
||||
return
|
||||
|
||||
await handle_set_widget(new_widget, message, state, bot)
|
||||
return
|
||||
|
||||
# Вариант 2: Callback query или пустая команда → запускаем FSM
|
||||
await status_clear(update=message, state=state)
|
||||
text: str = _(
|
||||
"📝 <b>Смена виджета бота</b>\n\n"
|
||||
"Текущий виджет: <i>{current}</i>\n\n"
|
||||
"Пожалуйста, введите новый виджет для бота (максимум 512 символов):"
|
||||
).format(current=current_widget)
|
||||
|
||||
await msg(update=message, text=text, markup=settings_keyboard(), state=state)
|
||||
await state.set_state(SetWidgetForm.new_widget)
|
||||
|
||||
|
||||
@router.message(SetWidgetForm.new_widget, IsOwner())
|
||||
async def process_new_widget(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
bot: Bot
|
||||
) -> None:
|
||||
"""
|
||||
Обрабатывает ввод нового текста виджета через FSM.
|
||||
"""
|
||||
new_widget: str = message.text.strip()
|
||||
|
||||
# Проверяем, что пользователь что-то ввёл
|
||||
if not new_widget:
|
||||
await message.answer(_("❌ Пожалуйста, введите корректный виджет."))
|
||||
return
|
||||
|
||||
await handle_set_widget(new_widget, message, state, bot)
|
||||
48
bot/handlers/commands/settings/settings_cmd.py
Normal file
48
bot/handlers/commands/settings/settings_cmd.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.i18n import gettext as _
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.filters import IsOwner
|
||||
from bot.templates import msg
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router", "settings_keyboard",)
|
||||
CMD: str = "settings".lower()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
def settings_keyboard() -> InlineKeyboardBuilder:
|
||||
"""Клавиатура настроек"""
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="🔙 Вернуться", callback_data="settings"))
|
||||
return ikb
|
||||
|
||||
|
||||
@router.callback_query(F.data.lower() == CMD, IsOwner())
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True), IsOwner())
|
||||
async def settings_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /settings"""
|
||||
await status_clear(update=message, state=state)
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Имя бота⚜️", callback_data='set_name'))
|
||||
ikb.row(InlineKeyboardButton(text="Описание бота📝", callback_data='set_description'))
|
||||
ikb.row(InlineKeyboardButton(text="Виджет🧩", callback_data='set_widget'))
|
||||
ikb.row(InlineKeyboardButton(text="Назад◀️", callback_data='menu'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _("""
|
||||
⚙️ Настройки
|
||||
"""
|
||||
).format(
|
||||
)
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg(update=message, text=text, markup=ikb, state=state)
|
||||
33
bot/handlers/commands/users/__init__.py
Normal file
33
bot/handlers/commands/users/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .start_cmd import router as start_cmd_router
|
||||
from .listwords import router as listwords_cmd_router
|
||||
from .word import router as word_cmd_router
|
||||
from .slience import router as slice_router
|
||||
from .conflict import router as conflict_router
|
||||
from .stats import router as stats_router
|
||||
from .report import router as report_router
|
||||
from .admins import router as admin_router
|
||||
from .notifications import router as notifications_router
|
||||
from .id import router as id_router
|
||||
from .emoji import router as emoji_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
notifications_router,
|
||||
report_router,
|
||||
admin_router,
|
||||
start_cmd_router,
|
||||
listwords_cmd_router,
|
||||
word_cmd_router,
|
||||
slice_router,
|
||||
conflict_router,
|
||||
stats_router,
|
||||
id_router,
|
||||
emoji_router,
|
||||
)
|
||||
434
bot/handlers/commands/users/admins.py
Normal file
434
bot/handlers/commands/users/admins.py
Normal file
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
Обработчики команд управления администраторами
|
||||
"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.filters.admin import IsSuperAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="admin_management_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def parse_user_id(text: str, command: str) -> tuple[bool, str | int]:
|
||||
"""
|
||||
Парсит ID пользователя из команды.
|
||||
|
||||
Args:
|
||||
text: Полный текст сообщения
|
||||
command: Название команды
|
||||
|
||||
Returns:
|
||||
(success, result): result это либо user_id (int), либо текст ошибки (str)
|
||||
"""
|
||||
parts = text.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
return False, f"❌ Использование: <code>/{command} <ID></code>"
|
||||
|
||||
user_id_str = parts[1].strip()
|
||||
|
||||
# Валидация ID
|
||||
try:
|
||||
user_id = int(user_id_str)
|
||||
|
||||
if user_id <= 0:
|
||||
return False, "❌ ID должен быть положительным числом"
|
||||
|
||||
if user_id > 9999999999: # Максимальный Telegram ID
|
||||
return False, "❌ Некорректный ID пользователя"
|
||||
|
||||
return True, user_id
|
||||
|
||||
except ValueError:
|
||||
return False, "❌ ID должен быть числом"
|
||||
|
||||
|
||||
def format_admin_info(user_id: int, username: str | None = None) -> str:
|
||||
"""Форматирует информацию об админе"""
|
||||
if username:
|
||||
return f"<code>{user_id}</code> (@{username})"
|
||||
return f"<code>{user_id}</code>"
|
||||
|
||||
|
||||
def get_refresh_admins_kb():
|
||||
"""Клавиатура для обновления списка админов"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="🔄 Обновить", callback_data="listadmins:refresh")
|
||||
ikb.button(text="➕ Добавить", callback_data="admin:help_add")
|
||||
ikb.adjust(2)
|
||||
return ikb.as_markup()
|
||||
|
||||
|
||||
# ================= ДОБАВЛЕНИЕ АДМИНИСТРАТОРА =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addadmin", ["addadmin"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsSuperAdmin())
|
||||
@log_action(action_name="ADD_ADMIN", log_args=True)
|
||||
async def add_admin_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет нового администратора бота.
|
||||
|
||||
Доступно только владельцам бота (OWNER_ID).
|
||||
|
||||
Использование: /addadmin <ID>
|
||||
Пример: /addadmin 123456789
|
||||
"""
|
||||
success, result = parse_user_id(message.text, "addadmin")
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
user_id = result
|
||||
|
||||
# Проверка: нельзя добавить самого себя
|
||||
if user_id == message.from_user.id:
|
||||
await message.answer(
|
||||
"⚠️ <b>Вы уже владелец бота</b>\n\n"
|
||||
"Вам не нужно добавлять себя в администраторы",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Проверка: нельзя добавить другого владельца
|
||||
if user_id in settings.OWNER_ID:
|
||||
await message.answer(
|
||||
"⚠️ <b>Этот пользователь уже владелец бота</b>\n\n"
|
||||
"Владельцы имеют полные права автоматически",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем, уже админ ли
|
||||
is_already_admin = await manager.is_admin(user_id)
|
||||
|
||||
if is_already_admin:
|
||||
await message.answer(
|
||||
f"⚠️ Пользователь {format_admin_info(user_id)} уже является администратором",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Добавляем администратора
|
||||
added = await manager.add_admin(
|
||||
user_id=user_id,
|
||||
added_by=message.from_user.id
|
||||
)
|
||||
|
||||
if added:
|
||||
text = (
|
||||
f"✅ <b>Администратор добавлен</b>\n\n"
|
||||
f"👤 ID: {format_admin_info(user_id)}\n"
|
||||
f"👑 Добавил: {format_admin_info(message.from_user.id, message.from_user.username)}\n\n"
|
||||
f"📋 Права администратора:\n"
|
||||
f"├─ Управление банвордами\n"
|
||||
f"├─ Просмотр статистики\n"
|
||||
f"├─ Активация режимов модерации\n"
|
||||
f"└─ Все команды бота\n\n"
|
||||
f"⚠️ <i>Не может управлять другими админами</i>\n"
|
||||
f"Список админов: /listadmins"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Администратор добавлен: {user_id} (добавил: {message.from_user.id})",
|
||||
log_type="ADMIN_MGMT"
|
||||
)
|
||||
else:
|
||||
text = "❌ <b>Ошибка добавления администратора</b>\n\nПопробуйте позже"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления администратора: {e}", log_type="ADMIN_MGMT")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= УДАЛЕНИЕ АДМИНИСТРАТОРА =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("remadmin", ["remadmin"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsSuperAdmin())
|
||||
@log_action(action_name="REMOVE_ADMIN", log_args=True)
|
||||
async def remove_admin_cmd(message: Message) -> None:
|
||||
"""
|
||||
Удаляет администратора бота.
|
||||
|
||||
Доступно только владельцам бота (OWNER_ID).
|
||||
|
||||
Использование: /remadmin <ID>
|
||||
Пример: /remadmin 123456789
|
||||
"""
|
||||
success, result = parse_user_id(message.text, "remadmin")
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
user_id = result
|
||||
|
||||
# Проверка: нельзя удалить владельца
|
||||
if user_id in settings.OWNER_ID:
|
||||
await message.answer(
|
||||
"⚠️ <b>Нельзя удалить владельца</b>\n\n"
|
||||
"Владельцы имеют права постоянно",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Проверка: нельзя удалить самого себя (если вы владелец)
|
||||
if user_id == message.from_user.id:
|
||||
await message.answer(
|
||||
"⚠️ <b>Нельзя удалить самого себя</b>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем, является ли администратором
|
||||
is_admin = await manager.is_admin(user_id)
|
||||
|
||||
if not is_admin:
|
||||
await message.answer(
|
||||
f"⚠️ Пользователь {format_admin_info(user_id)} не является администратором",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Удаляем администратора
|
||||
removed = await manager.remove_admin(user_id=user_id)
|
||||
|
||||
if removed:
|
||||
text = (
|
||||
f"🗑 <b>Администратор удалён</b>\n\n"
|
||||
f"👤 ID: {format_admin_info(user_id)}\n"
|
||||
f"👑 Удалил: {format_admin_info(message.from_user.id, message.from_user.username)}\n\n"
|
||||
f"⚠️ <i>Пользователь больше не имеет доступа к командам бота</i>"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Администратор удалён: {user_id} (удалил: {message.from_user.id})",
|
||||
log_type="ADMIN_MGMT"
|
||||
)
|
||||
else:
|
||||
text = "❌ <b>Ошибка удаления администратора</b>\n\nПопробуйте позже"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления администратора: {e}", log_type="ADMIN_MGMT")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= СПИСОК АДМИНИСТРАТОРОВ =================
|
||||
|
||||
@router.callback_query(F.data == "listadmins:refresh")
|
||||
@router.message(Command(*COMMANDS.get("listadmins", ["listadmins"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsSuperAdmin())
|
||||
@log_action(action_name="LIST_ADMINS")
|
||||
async def list_admins_cmd(update: Message | CallbackQuery) -> None:
|
||||
"""
|
||||
Показывает список всех администраторов бота.
|
||||
|
||||
Доступно только владельцам бота (OWNER_ID).
|
||||
|
||||
Использование: /listadmins
|
||||
"""
|
||||
# Определяем тип update
|
||||
if isinstance(update, CallbackQuery):
|
||||
message = update.message
|
||||
is_callback = True
|
||||
else:
|
||||
message = update
|
||||
is_callback = False
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Получаем всех админов из БД
|
||||
db_admins = await manager.repo.get_admins()
|
||||
|
||||
# Получаем статистику
|
||||
stats = await manager.get_stats()
|
||||
|
||||
# === ФОРМИРУЕМ ВЫВОД ===
|
||||
|
||||
output = "👥 <b>СПИСОК АДМИНИСТРАТОРОВ</b>\n\n"
|
||||
|
||||
# Владельцы (OWNER_ID)
|
||||
output += "👑 <b>Владельцы бота</b> (полные права):\n"
|
||||
for owner_id in settings.OWNER_ID:
|
||||
output += f"├─ <code>{owner_id}</code>\n"
|
||||
output += "\n"
|
||||
|
||||
# Администраторы из БД
|
||||
if db_admins:
|
||||
output += f"⚙️ <b>Администраторы</b> ({len(db_admins)}):\n"
|
||||
|
||||
for admin_id in sorted(db_admins):
|
||||
output += f"├─ <code>{admin_id}</code>\n"
|
||||
|
||||
output += "\n"
|
||||
output += "📋 <b>Права администраторов:</b>\n"
|
||||
output += "├─ Управление банвордами\n"
|
||||
output += "├─ Просмотр статистики\n"
|
||||
output += "├─ Активация режимов модерации\n"
|
||||
output += "└─ Все команды бота (кроме управления админами)\n\n"
|
||||
else:
|
||||
output += "⚙️ <b>Администраторы:</b>\n"
|
||||
output += "└─ <i>Нет дополнительных администраторов</i>\n\n"
|
||||
|
||||
# Общая статистика
|
||||
total_admins = len(settings.OWNER_ID) + len(db_admins)
|
||||
output += f"📊 <b>Итого:</b> {total_admins} администратор(ов)\n\n"
|
||||
|
||||
# Команды управления
|
||||
output += "🔧 <b>Управление:</b>\n"
|
||||
output += "• /addadmin <code>ID</code> — добавить админа\n"
|
||||
output += "• /remadmin <code>ID</code> — удалить админа\n\n"
|
||||
|
||||
output += "💡 <i>Только владельцы могут управлять администраторами</i>"
|
||||
|
||||
# Клавиатура
|
||||
keyboard = get_refresh_admins_kb()
|
||||
|
||||
# Отправка
|
||||
if is_callback:
|
||||
await message.edit_text(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
await update.answer("✅ Список обновлён")
|
||||
else:
|
||||
await message.answer(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения списка администраторов: {e}", log_type="ADMIN_MGMT")
|
||||
|
||||
error_text = "❌ <b>Ошибка загрузки списка</b>\n\nПопробуйте позже"
|
||||
|
||||
if is_callback:
|
||||
await update.answer("❌ Ошибка загрузки", show_alert=True)
|
||||
else:
|
||||
await message.answer(error_text, parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ CALLBACK =================
|
||||
|
||||
@router.callback_query(F.data == "admin:help_add")
|
||||
async def admin_help_add_callback(callback: CallbackQuery) -> None:
|
||||
"""Показывает помощь по добавлению админа"""
|
||||
text = (
|
||||
"➕ <b>Как добавить администратора?</b>\n\n"
|
||||
"1️⃣ Узнайте Telegram ID пользователя\n"
|
||||
" • Используйте бота @userinfobot\n"
|
||||
" • Или попросите пользователя написать /start\n\n"
|
||||
"2️⃣ Выполните команду:\n"
|
||||
" <code>/addadmin ID</code>\n\n"
|
||||
"Пример:\n"
|
||||
"<code>/addadmin 123456789</code>"
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
await callback.message.answer(text, parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("adminhelp", ["adminhelp"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsSuperAdmin())
|
||||
async def admin_help_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает подробную справку по управлению администраторами.
|
||||
|
||||
Использование: /adminhelp
|
||||
"""
|
||||
text = (
|
||||
"👥 <b>УПРАВЛЕНИЕ АДМИНИСТРАТОРАМИ</b>\n\n"
|
||||
"🔐 <b>Уровни доступа:</b>\n\n"
|
||||
"👑 <b>Владельцы</b> (OWNER_ID):\n"
|
||||
"├─ Все права администратора\n"
|
||||
"├─ Управление другими админами\n"
|
||||
"└─ Указываются в конфигурации\n\n"
|
||||
"⚙️ <b>Администраторы:</b>\n"
|
||||
"├─ Управление банвордами\n"
|
||||
"├─ Просмотр статистики\n"
|
||||
"├─ Активация режимов модерации\n"
|
||||
"└─ НЕ могут управлять админами\n\n"
|
||||
"📝 <b>Команды:</b>\n"
|
||||
"• /listadmins — список всех админов\n"
|
||||
"• /addadmin <code>ID</code> — добавить админа\n"
|
||||
"• /remadmin <code>ID</code> — удалить админа\n\n"
|
||||
"💡 <b>Как узнать ID пользователя?</b>\n"
|
||||
"• Используйте бота @userinfobot\n"
|
||||
"• Попросите пользователя написать боту\n"
|
||||
"• ID отображается в логах бота\n\n"
|
||||
"⚠️ <b>Важно:</b>\n"
|
||||
"├─ Нельзя удалить владельца\n"
|
||||
"├─ Нельзя удалить самого себя\n"
|
||||
"└─ Все действия логируются"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("checkadmin", ["checkadmin"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsSuperAdmin())
|
||||
@log_action(action_name="CHECK_ADMIN")
|
||||
async def check_admin_cmd(message: Message) -> None:
|
||||
"""
|
||||
Проверяет, является ли пользователь администратором.
|
||||
|
||||
Использование: /checkadmin <ID>
|
||||
"""
|
||||
success, result = parse_user_id(message.text, "checkadmin")
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
user_id = result
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем статус
|
||||
is_owner = user_id in settings.OWNER_ID
|
||||
is_db_admin = await manager.is_admin(user_id)
|
||||
|
||||
text = f"🔍 <b>Проверка пользователя</b>\n\n"
|
||||
text += f"👤 ID: <code>{user_id}</code>\n\n"
|
||||
|
||||
if is_owner:
|
||||
text += "👑 Статус: <b>Владелец бота</b>\n"
|
||||
text += "✅ Полные права администратора\n"
|
||||
text += "✅ Может управлять админами"
|
||||
elif is_db_admin:
|
||||
text += "⚙️ Статус: <b>Администратор</b>\n"
|
||||
text += "✅ Доступ к командам бота\n"
|
||||
text += "❌ Не может управлять админами"
|
||||
else:
|
||||
text += "👤 Статус: <b>Обычный пользователь</b>\n"
|
||||
text += "❌ Нет прав администратора\n\n"
|
||||
text += f"Добавить в админы: <code>/addadmin {user_id}</code>"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка проверки администратора: {e}", log_type="ADMIN_MGMT")
|
||||
await message.answer("❌ <b>Ошибка проверки</b>", parse_mode="HTML")
|
||||
435
bot/handlers/commands/users/conflict.py
Normal file
435
bot/handlers/commands/users/conflict.py
Normal file
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
Обработчики команд режима антиконфликта
|
||||
"""
|
||||
from datetime import datetime
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from database.models import BanWordType
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="conflict_mode_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def parse_conflict_args(text: str, command: str, need_minutes: bool = False) -> tuple[bool, str | list]:
|
||||
"""
|
||||
Парсит аргументы команды для конфликтного режима.
|
||||
|
||||
Args:
|
||||
text: Полный текст сообщения
|
||||
command: Название команды
|
||||
need_minutes: Требуется ли параметр минут
|
||||
|
||||
Returns:
|
||||
(success, result): result это либо список аргументов, либо текст ошибки
|
||||
"""
|
||||
parts = text.split(maxsplit=2 if need_minutes else 1)
|
||||
|
||||
min_args = 1 if need_minutes else 1
|
||||
|
||||
if len(parts) < min_args + 1:
|
||||
if need_minutes:
|
||||
return False, f"❌ Использование: <code>/{command} [минуты]</code>"
|
||||
else:
|
||||
return False, f"❌ Использование: <code>/{command} [слово]</code>"
|
||||
|
||||
args = parts[1:]
|
||||
|
||||
# Валидация слова
|
||||
if not need_minutes:
|
||||
if len(args[0]) < 2:
|
||||
return False, "❌ Слово должно содержать минимум 2 символа"
|
||||
|
||||
if len(args[0]) > 100:
|
||||
return False, "❌ Слово слишком длинное (максимум 100 символов)"
|
||||
|
||||
return True, args
|
||||
|
||||
|
||||
def format_time_str(minutes: int) -> str:
|
||||
"""Форматирует время в читабельный формат"""
|
||||
if minutes < 60:
|
||||
return f"{minutes} мин"
|
||||
elif minutes < 1440:
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
return f"{hours}ч {mins}м" if mins else f"{hours}ч"
|
||||
else:
|
||||
days = minutes // 1440
|
||||
hours = (minutes % 1440) // 60
|
||||
return f"{days}д {hours}ч" if hours else f"{days}д"
|
||||
|
||||
|
||||
def format_datetime(dt: datetime) -> str:
|
||||
"""Форматирует datetime в читабельный формат"""
|
||||
return dt.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
|
||||
# ================= ДОБАВЛЕНИЕ КОНФЛИКТНЫХ СЛОВ =================
|
||||
|
||||
@router.message(
|
||||
Command(*COMMANDS.get("addconflictword", ["addconflictword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="ADD_CONFLICT_WORD", log_args=True)
|
||||
async def add_conflict_word_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет конфликтное слово-подстроку.
|
||||
|
||||
Конфликтные слова работают только в режиме /stopconflict.
|
||||
|
||||
Использование: /addconflictword <слово>
|
||||
"""
|
||||
success, result = parse_conflict_args(message.text, "addconflictword", need_minutes=False)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.CONFLICT_SUBSTRING,
|
||||
added_by=message.from_user.id,
|
||||
reason="Конфликтное слово"
|
||||
)
|
||||
|
||||
if added:
|
||||
text = (
|
||||
f"✅ <b>Конфликтное слово добавлено</b>\n\n"
|
||||
f"📝 Слово: <code>{word}</code>\n"
|
||||
f"🔍 Тип: подстрока\n\n"
|
||||
f"⚔️ <i>Будет работать только в режиме антиконфликта</i>\n"
|
||||
f"Активируйте: <code>/stopconflict [минуты]</code>"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Конфликтное слово <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления конфликтного слова: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(
|
||||
Command(*COMMANDS.get("addconflictlemma", ["addconflictlemma"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="ADD_CONFLICT_LEMMA", log_args=True)
|
||||
async def add_conflict_lemma_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет конфликтную лемму.
|
||||
|
||||
Конфликтные леммы работают только в режиме /stopconflict.
|
||||
|
||||
Использование: /addconflictlemma <слово>
|
||||
"""
|
||||
success, result = parse_conflict_args(message.text, "addconflictlemma", need_minutes=False)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.CONFLICT_LEMMA,
|
||||
added_by=message.from_user.id,
|
||||
reason="Конфликтная лемма"
|
||||
)
|
||||
|
||||
if added:
|
||||
text = (
|
||||
f"✅ <b>Конфликтная лемма добавлена</b>\n\n"
|
||||
f"🔤 Слово: <code>{word}</code>\n"
|
||||
f"🔍 Тип: лемма (все формы слова)\n\n"
|
||||
f"⚔️ <i>Будет работать только в режиме антиконфликта</i>\n"
|
||||
f"Активируйте: <code>/stopconflict [минуты]</code>"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Конфликтная лемма <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления конфликтной леммы: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= УДАЛЕНИЕ КОНФЛИКТНЫХ СЛОВ =================
|
||||
|
||||
@router.message(
|
||||
Command(*COMMANDS.get("remconflictword", ["remconflictword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="REMOVE_CONFLICT_WORD", log_args=True)
|
||||
async def remove_conflict_word_cmd(message: Message) -> None:
|
||||
"""
|
||||
Удаляет конфликтное слово-подстроку.
|
||||
|
||||
Использование: /remconflictword <слово>
|
||||
"""
|
||||
success, result = parse_conflict_args(message.text, "remconflictword", need_minutes=False)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.CONFLICT_SUBSTRING
|
||||
)
|
||||
|
||||
if removed:
|
||||
text = f"🗑 <b>Конфликтное слово удалено</b>\n\n📝 Слово: <code>{word}</code>"
|
||||
else:
|
||||
text = f"⚠️ Конфликтное слово <code>{word}</code> не найдено"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления конфликтного слова: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(
|
||||
Command(*COMMANDS.get("remconflictlemma", ["remconflictlemma"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="REMOVE_CONFLICT_LEMMA", log_args=True)
|
||||
async def remove_conflict_lemma_cmd(message: Message) -> None:
|
||||
"""
|
||||
Удаляет конфликтную лемму.
|
||||
|
||||
Использование: /remconflictlemma <слово>
|
||||
"""
|
||||
success, result = parse_conflict_args(message.text, "remconflictlemma", need_minutes=False)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.CONFLICT_LEMMA
|
||||
)
|
||||
|
||||
if removed:
|
||||
text = f"🗑 <b>Конфликтная лемма удалена</b>\n\n🔤 Слово: <code>{word}</code>"
|
||||
else:
|
||||
text = f"⚠️ Конфликтная лемма <code>{word}</code> не найдена"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления конфликтной леммы: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= УПРАВЛЕНИЕ РЕЖИМОМ АНТИКОНФЛИКТА =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("stopconflict", ["stopconflict"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="START_CONFLICT_MODE", log_args=True)
|
||||
async def start_conflict_mode_cmd(message: Message) -> None:
|
||||
"""
|
||||
Активирует режим антиконфликта на указанное время.
|
||||
|
||||
В этом режиме работают только конфликтные слова/леммы.
|
||||
Обычные банворды временно отключаются.
|
||||
|
||||
Использование: /stopconflict <минуты>
|
||||
Пример: /stopconflict 30
|
||||
"""
|
||||
success, result = parse_conflict_args(message.text, "stopconflict", need_minutes=True)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
# Валидация минут
|
||||
try:
|
||||
minutes = int(result[0])
|
||||
if minutes < 1 or minutes > 10080: # Максимум неделя
|
||||
await message.answer(
|
||||
"❌ Время должно быть от 1 минуты до 10080 минут (7 дней)",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Получаем статистику конфликтных слов
|
||||
data = await manager.get_all_words_list()
|
||||
conflict_words_count = len(data.get('conflict_substring', set()))
|
||||
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
|
||||
total_conflict = conflict_words_count + conflict_lemmas_count
|
||||
|
||||
if total_conflict == 0:
|
||||
await message.answer(
|
||||
"⚠️ <b>Нет конфликтных слов</b>\n\n"
|
||||
"Сначала добавьте конфликтные слова:\n"
|
||||
"• <code>/addconflictword [слово]</code>\n"
|
||||
"• <code>/addconflictlemma [слово]</code>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Активируем режим
|
||||
expires_at = await manager.set_conflict_mode(minutes)
|
||||
|
||||
time_str = format_time_str(minutes)
|
||||
expires_str = format_datetime(expires_at)
|
||||
|
||||
text = (
|
||||
f"⚔️ <b>РЕЖИМ АНТИКОНФЛИКТА АКТИВИРОВАН</b>\n\n"
|
||||
f"⏱ Длительность: {time_str}\n"
|
||||
f"🕐 Окончание: {expires_str}\n\n"
|
||||
f"📊 Активные правила:\n"
|
||||
f"├─ Конфликтные слова: <code>{conflict_words_count}</code>\n"
|
||||
f"└─ Конфликтные леммы: <code>{conflict_lemmas_count}</code>\n\n"
|
||||
f"⚠️ <i>Обычные банворды временно отключены</i>\n"
|
||||
f"Отключить режим: /unstopconflict"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
logger.info(
|
||||
f"Режим антиконфликта активирован на {minutes} мин "
|
||||
f"(конфликтных правил: {total_conflict})",
|
||||
log_type="CONFLICT"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка активации режима антиконфликта: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка активации режима</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("unstopconflict", ["unstopconflict"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="STOP_CONFLICT_MODE")
|
||||
async def stop_conflict_mode_cmd(message: Message) -> None:
|
||||
"""
|
||||
Отключает режим антиконфликта.
|
||||
|
||||
Использование: /unstopconflict
|
||||
"""
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем, активен ли режим
|
||||
is_active = await manager.is_conflict_active()
|
||||
|
||||
if not is_active:
|
||||
await message.answer(
|
||||
"⚠️ <b>Режим антиконфликта не активен</b>\n\n"
|
||||
"Активируйте: <code>/stopconflict [минуты]</code>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Отключаем режим
|
||||
await manager.disable_conflict_mode()
|
||||
|
||||
text = (
|
||||
f"✅ <b>Режим антиконфликта отключен</b>\n\n"
|
||||
f"🔄 Обычные банворды снова активны\n"
|
||||
f"⚔️ Конфликтные слова деактивированы"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
logger.info("Режим антиконфликта отключён", log_type="CONFLICT")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отключения режима антиконфликта: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка отключения режима</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("conflictstatus", ["conflictstatus"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="CONFLICT_STATUS")
|
||||
async def conflict_status_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает статус режима антиконфликта.
|
||||
|
||||
Использование: /conflictstatus
|
||||
"""
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем активность режима
|
||||
is_active = await manager.is_conflict_active()
|
||||
|
||||
# Получаем статистику
|
||||
data = await manager.get_all_words_list()
|
||||
conflict_words_count = len(data.get('conflict_substring', set()))
|
||||
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
|
||||
total_conflict = conflict_words_count + conflict_lemmas_count
|
||||
|
||||
if is_active:
|
||||
# Режим активен - показываем детали
|
||||
conflict_until_str = await manager.repo.get_setting("conflict_until")
|
||||
conflict_until = float(conflict_until_str)
|
||||
expires_at = datetime.fromtimestamp(conflict_until)
|
||||
|
||||
now = datetime.now()
|
||||
time_left_seconds = (expires_at - now).total_seconds()
|
||||
time_left_minutes = int(time_left_seconds / 60)
|
||||
|
||||
text = (
|
||||
f"⚔️ <b>РЕЖИМ АНТИКОНФЛИКТА АКТИВЕН</b>\n\n"
|
||||
f"⏱ Осталось: {format_time_str(time_left_minutes)}\n"
|
||||
f"🕐 Окончание: {format_datetime(expires_at)}\n\n"
|
||||
f"📊 Активные правила:\n"
|
||||
f"├─ Конфликтные слова: <code>{conflict_words_count}</code>\n"
|
||||
f"└─ Конфликтные леммы: <code>{conflict_lemmas_count}</code>\n\n"
|
||||
f"⚠️ <i>Обычные банворды отключены</i>\n"
|
||||
f"Отключить: /unstopconflict"
|
||||
)
|
||||
else:
|
||||
# Режим не активен
|
||||
text = (
|
||||
f"💤 <b>Режим антиконфликта НЕ активен</b>\n\n"
|
||||
f"📊 Конфликтных правил в базе:\n"
|
||||
f"├─ Слова: <code>{conflict_words_count}</code>\n"
|
||||
f"└─ Леммы: <code>{conflict_lemmas_count}</code>\n\n"
|
||||
)
|
||||
|
||||
if total_conflict > 0:
|
||||
text += f"Активировать: <code>/stopconflict [минуты]</code>"
|
||||
else:
|
||||
text += (
|
||||
f"⚠️ <i>Нет конфликтных слов</i>\n"
|
||||
f"Добавьте:\n"
|
||||
f"• <code>/addconflictword [слово]</code>\n"
|
||||
f"• <code>/addconflictlemma [слово]</code>"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статуса режима: {e}", log_type="CONFLICT")
|
||||
await message.answer("❌ <b>Ошибка получения статуса</b>", parse_mode="HTML")
|
||||
215
bot/handlers/commands/users/emoji.py
Normal file
215
bot/handlers/commands/users/emoji.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Обработчик команды /emoji для извлечения ID премиум эмодзи
|
||||
"""
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="emoji_extractor_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def extract_custom_emojis(message: Message) -> list[dict]:
|
||||
"""
|
||||
Извлекает все кастомные эмодзи из сообщения.
|
||||
|
||||
Args:
|
||||
message: Сообщение для анализа
|
||||
|
||||
Returns:
|
||||
Список словарей с информацией об эмодзи
|
||||
"""
|
||||
if not message.entities and not message.caption_entities:
|
||||
return []
|
||||
|
||||
# Определяем текст и entities
|
||||
text = message.text or message.caption
|
||||
entities = message.entities or message.caption_entities
|
||||
|
||||
if not text or not entities:
|
||||
return []
|
||||
|
||||
custom_emojis = []
|
||||
|
||||
for entity in entities:
|
||||
if entity.type == "custom_emoji":
|
||||
# Извлекаем символ эмодзи
|
||||
emoji_char = text[entity.offset:entity.offset + entity.length]
|
||||
|
||||
custom_emojis.append({
|
||||
"char": emoji_char,
|
||||
"id": entity.custom_emoji_id,
|
||||
"offset": entity.offset
|
||||
})
|
||||
|
||||
return custom_emojis
|
||||
|
||||
|
||||
def format_emoji_html(emoji_char: str, emoji_id: str) -> str:
|
||||
"""
|
||||
Форматирует эмодзи в HTML-тег.
|
||||
|
||||
Args:
|
||||
emoji_char: Символ эмодзи (fallback)
|
||||
emoji_id: ID кастомного эмодзи
|
||||
|
||||
Returns:
|
||||
HTML-строка
|
||||
"""
|
||||
return f'<tg-emoji emoji-id="{emoji_id}">{emoji_char}</tg-emoji>'
|
||||
|
||||
|
||||
def escape_html(text: str) -> str:
|
||||
"""Экранирует HTML символы"""
|
||||
return (
|
||||
text.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
)
|
||||
|
||||
|
||||
# ================= КОМАНДА /EMOJI =================
|
||||
|
||||
@router.message(
|
||||
Command(*COMMANDS.get("emoji", ["emoji"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin()
|
||||
)
|
||||
async def emoji_extractor_cmd(message: Message) -> None:
|
||||
"""
|
||||
Извлекает кастомные эмодзи из сообщения.
|
||||
|
||||
Доступно только администраторам.
|
||||
|
||||
Использование: /emoji (в ответ на сообщение)
|
||||
"""
|
||||
# Проверяем, что команда в ответ на сообщение
|
||||
if not message.reply_to_message:
|
||||
await message.answer(
|
||||
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
|
||||
"📝 Как использовать:\n"
|
||||
"1. Ответьте на сообщение с премиум эмодзи\n"
|
||||
"2. Напишите <code>/emoji</code>\n\n"
|
||||
"💡 <i>Бот извлечёт все кастомные эмодзи и покажет HTML-код</i>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
replied_message = message.reply_to_message
|
||||
|
||||
# Извлекаем кастомные эмодзи
|
||||
custom_emojis = extract_custom_emojis(replied_message)
|
||||
|
||||
if not custom_emojis:
|
||||
# Нет кастомных эмодзи
|
||||
await message.answer(
|
||||
"⚠️ <b>Кастомные эмодзи не найдены</b>\n\n"
|
||||
"В этом сообщении нет премиум эмодзи.\n\n"
|
||||
"💡 <i>Попробуйте ответить на сообщение с анимированными эмодзи</i>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# === ФОРМИРУЕМ ОТВЕТ ===
|
||||
|
||||
output = f"✨ <b>НАЙДЕНО ЭМОДЗИ: {len(custom_emojis)}</b>\n\n"
|
||||
|
||||
for idx, emoji_data in enumerate(custom_emojis, 1):
|
||||
emoji_char = emoji_data["char"]
|
||||
emoji_id = emoji_data["id"]
|
||||
|
||||
output += f"<b>{idx}.</b> Эмодзи: {emoji_char}\n"
|
||||
output += f"📋 <b>ID:</b> <code>{emoji_id}</code>\n\n"
|
||||
|
||||
# HTML-код (экранированный для отображения)
|
||||
html_code = format_emoji_html(emoji_char, emoji_id)
|
||||
html_escaped = escape_html(html_code)
|
||||
|
||||
output += f"📝 <b>HTML-код:</b>\n"
|
||||
output += f"<code>{html_escaped}</code>\n\n"
|
||||
|
||||
# Пример использования
|
||||
output += f"🎨 <b>Превью:</b> {html_code}\n"
|
||||
|
||||
if idx < len(custom_emojis):
|
||||
output += "\n" + "─" * 30 + "\n\n"
|
||||
|
||||
output += "💡 <i>Скопируйте HTML-код и используйте в своих сообщениях</i>"
|
||||
|
||||
# Создаём клавиатуру
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="✖️ Закрыть", callback_data="emoji_close")
|
||||
|
||||
# Отправляем
|
||||
try:
|
||||
await message.answer(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Извлечено {len(custom_emojis)} кастомных эмодзи админом {message.from_user.id}",
|
||||
log_type="EMOJI_EXTRACT"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки эмодзи: {e}", log_type="ERROR")
|
||||
await message.answer(
|
||||
"❌ <b>Ошибка извлечения эмодзи</b>\n\n"
|
||||
"Попробуйте позже или обратитесь к разработчику.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
# ================= ОБРАБОТЧИК КНОПКИ ЗАКРЫТИЯ =================
|
||||
|
||||
@router.callback_query(lambda c: c.data == "emoji_close", IsAdmin())
|
||||
async def emoji_close_callback(callback) -> None:
|
||||
"""Закрывает сообщение с эмодзи"""
|
||||
try:
|
||||
await callback.message.delete()
|
||||
await callback.answer("✅ Закрыто")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления сообщения с эмодзи: {e}", log_type="ERROR")
|
||||
await callback.answer("❌ Не удалось удалить", show_alert=True)
|
||||
|
||||
|
||||
# ================= ДОПОЛНИТЕЛЬНАЯ КОМАНДА /EMOJIHELP =================
|
||||
|
||||
@router.message(
|
||||
Command(*COMMANDS.get("emojihelp", ["emojihelp"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin()
|
||||
)
|
||||
async def emoji_help_cmd(message: Message) -> None:
|
||||
"""
|
||||
Справка по работе с кастомными эмодзи.
|
||||
"""
|
||||
text = (
|
||||
"🎨 <b>РАБОТА С КАСТОМНЫМИ ЭМОДЗИ</b>\n\n"
|
||||
"📝 <b>Команда /emoji</b>\n"
|
||||
"Извлекает ID премиум эмодзи из сообщения\n\n"
|
||||
"🔧 <b>Как использовать:</b>\n"
|
||||
"1️⃣ Ответьте на сообщение с эмодзи\n"
|
||||
"2️⃣ Напишите <code>/emoji</code>\n"
|
||||
"3️⃣ Скопируйте HTML-код\n\n"
|
||||
"💻 <b>Формат HTML-кода:</b>\n"
|
||||
"<code><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"
|
||||
"⚠️ <b>Важно:</b>\n"
|
||||
"├─ Используйте <code>parse_mode=\"HTML\"</code>\n"
|
||||
"├─ Пользователи без Premium видят fallback\n"
|
||||
"└─ Работает только с кастомными эмодзи\n\n"
|
||||
"💡 <i>Попробуйте отправить эмодзи и ответить командой /emoji</i>"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
221
bot/handlers/commands/users/id.py
Normal file
221
bot/handlers/commands/users/id.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Обработчик команды /id для получения информации о пользователе
|
||||
"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from configs import settings, COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="user_id_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def get_close_keyboard():
|
||||
"""Создаёт клавиатуру с кнопкой закрытия"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="✖️ Закрыть", callback_data="id_close")
|
||||
return ikb.as_markup()
|
||||
|
||||
|
||||
# ================= КОМАНДА /ID =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("id", ["id"]), prefix=settings.PREFIX, ignore_case=True))
|
||||
async def id_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает информацию о вашем Telegram аккаунте.
|
||||
|
||||
Доступно всем пользователям.
|
||||
|
||||
Использование: /id
|
||||
"""
|
||||
user = message.from_user
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Не удалось получить информацию о пользователе")
|
||||
return
|
||||
|
||||
# === ФОРМИРУЕМ ИНФОРМАЦИЮ ===
|
||||
|
||||
output = "👤 <b>ИНФОРМАЦИЯ О ВАС</b>\n\n"
|
||||
|
||||
# Имя
|
||||
full_name_parts = []
|
||||
if user.first_name:
|
||||
full_name_parts.append(user.first_name)
|
||||
if user.last_name:
|
||||
full_name_parts.append(user.last_name)
|
||||
|
||||
full_name = " ".join(full_name_parts) if full_name_parts else "Не указано"
|
||||
output += f"📝 <b>Имя:</b> {full_name}\n"
|
||||
|
||||
# Username
|
||||
if user.username:
|
||||
output += f"🔗 <b>Username:</b> @{user.username}\n"
|
||||
else:
|
||||
output += f"🔗 <b>Username:</b> <i>не установлен</i>\n"
|
||||
|
||||
# ID
|
||||
output += f"🆔 <b>ID:</b> <code>{user.id}</code>\n\n"
|
||||
|
||||
# Тип аккаунта
|
||||
if user.is_bot:
|
||||
output += f"🤖 <b>Тип:</b> Бот\n"
|
||||
elif user.is_premium:
|
||||
output += f"⭐️ <b>Тип:</b> Premium пользователь\n"
|
||||
else:
|
||||
output += f"👥 <b>Тип:</b> Обычный пользователь\n"
|
||||
|
||||
# Дополнительная информация
|
||||
output += "\n📊 <b>Дополнительно:</b>\n"
|
||||
|
||||
# Язык
|
||||
if user.language_code:
|
||||
language_names = {
|
||||
'ru': '🇷🇺 Русский',
|
||||
'en': '🇬🇧 English',
|
||||
'uk': '🇺🇦 Українська',
|
||||
'de': '🇩🇪 Deutsch',
|
||||
'es': '🇪🇸 Español',
|
||||
'fr': '🇫🇷 Français',
|
||||
'it': '🇮🇹 Italiano',
|
||||
'pt': '🇵🇹 Português',
|
||||
}
|
||||
language = language_names.get(user.language_code, f"🌐 {user.language_code.upper()}")
|
||||
output += f"├─ Язык: {language}\n"
|
||||
|
||||
# Информация о чате
|
||||
if message.chat.type == "private":
|
||||
output += f"├─ Чат: 💬 Личные сообщения\n"
|
||||
else:
|
||||
chat_title = message.chat.title or "Без названия"
|
||||
chat_types = {
|
||||
"group": "👥 Группа",
|
||||
"supergroup": "👥 Супергруппа",
|
||||
"channel": "📢 Канал"
|
||||
}
|
||||
chat_type = chat_types.get(message.chat.type, "💬 Чат")
|
||||
output += f"├─ Чат: {chat_type}\n"
|
||||
output += f"├─ Название: {chat_title}\n"
|
||||
output += f"├─ Chat ID: <code>{message.chat.id}</code>\n"
|
||||
|
||||
# Получаем количество участников (только для групп)
|
||||
try:
|
||||
member_count = await message.bot.get_chat_member_count(message.chat.id)
|
||||
output += f"├─ Участников: {member_count}\n"
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось получить количество участников: {e}", log_type="USER_ID")
|
||||
|
||||
# Message ID
|
||||
output += f"└─ Message ID: <code>{message.message_id}</code>\n\n"
|
||||
|
||||
# Подсказка
|
||||
output += "💡 <i>Эту информацию видите только вы</i>"
|
||||
|
||||
# Клавиатура
|
||||
keyboard = get_close_keyboard()
|
||||
|
||||
# Отправляем
|
||||
try:
|
||||
await message.answer(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
logger.debug(f"Команда /id от пользователя {user.id}", log_type="USER_ID")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки информации о пользователе: {e}", log_type="ERROR")
|
||||
await message.answer("❌ Произошла ошибка при получении информации")
|
||||
|
||||
|
||||
# ================= ОБРАБОТЧИК КНОПКИ ЗАКРЫТИЯ =================
|
||||
|
||||
@router.callback_query(F.data == "id_close")
|
||||
async def id_close_callback(callback: CallbackQuery) -> None:
|
||||
"""Закрывает (удаляет) сообщение с информацией"""
|
||||
try:
|
||||
await callback.message.delete()
|
||||
await callback.answer("✅ Закрыто")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления сообщения ID: {e}", log_type="ERROR")
|
||||
await callback.answer("❌ Не удалось удалить сообщение", show_alert=True)
|
||||
|
||||
|
||||
# ================= КОМАНДА /MYID (АЛЬТЕРНАТИВА) =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("myid", ["myid"]), prefix=settings.PREFIX, ignore_case=True))
|
||||
async def myid_cmd(message: Message) -> None:
|
||||
"""
|
||||
Быстрый просмотр вашего ID.
|
||||
|
||||
Использование: /myid
|
||||
"""
|
||||
user = message.from_user
|
||||
|
||||
if not user:
|
||||
await message.answer("❌ Не удалось получить ID")
|
||||
return
|
||||
|
||||
# Короткий ответ
|
||||
text = f"🆔 Ваш ID: <code>{user.id}</code>"
|
||||
|
||||
if user.username:
|
||||
text += f"\n🔗 Username: @{user.username}"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= КОМАНДА /CHATID =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("chatid", ["chatid"]), prefix=settings.PREFIX, ignore_case=True))
|
||||
async def chatid_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает ID текущего чата.
|
||||
|
||||
Использование: /chatid
|
||||
"""
|
||||
chat = message.chat
|
||||
|
||||
output = "💬 <b>ИНФОРМАЦИЯ О ЧАТЕ</b>\n\n"
|
||||
|
||||
# Тип чата
|
||||
chat_types = {
|
||||
"private": "💬 Личные сообщения",
|
||||
"group": "👥 Группа",
|
||||
"supergroup": "👥 Супергруппа",
|
||||
"channel": "📢 Канал"
|
||||
}
|
||||
chat_type = chat_types.get(chat.type, "💬 Чат")
|
||||
|
||||
output += f"📝 <b>Тип:</b> {chat_type}\n"
|
||||
|
||||
if chat.title:
|
||||
output += f"📌 <b>Название:</b> {chat.title}\n"
|
||||
|
||||
if chat.username:
|
||||
output += f"🔗 <b>Username:</b> @{chat.username}\n"
|
||||
|
||||
output += f"🆔 <b>Chat ID:</b> <code>{chat.id}</code>\n"
|
||||
|
||||
# Дополнительная информация для групп
|
||||
if chat.type in ["group", "supergroup"]:
|
||||
try:
|
||||
member_count = await message.bot.get_chat_member_count(chat.id)
|
||||
output += f"👥 <b>Участников:</b> {member_count}\n"
|
||||
except Exception as e:
|
||||
logger.debug(f"Не удалось получить количество участников: {e}", log_type="USER_ID")
|
||||
|
||||
keyboard = get_close_keyboard()
|
||||
|
||||
await message.answer(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
238
bot/handlers/commands/users/listwords.py
Normal file
238
bot/handlers/commands/users/listwords.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""
|
||||
Обработчик команды /listwords - отображение всех правил модерации
|
||||
"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "list"
|
||||
router: Router = Router(name="listwords_cmd_router")
|
||||
|
||||
|
||||
def get_refresh_kb(page: int = 0):
|
||||
"""Клавиатура с кнопкой обновления"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="🔄 Обновить", callback_data=f"listwords:refresh:{page}")
|
||||
ikb.button(text="📊 Статистика", callback_data="stats")
|
||||
ikb.adjust(2)
|
||||
return ikb.as_markup()
|
||||
|
||||
|
||||
async def format_banwords_list(page: int = 0) -> str:
|
||||
"""
|
||||
Форматирует список всех банвордов с разбивкой по типам.
|
||||
|
||||
Args:
|
||||
page: Номер страницы (для будущей пагинации)
|
||||
|
||||
Returns:
|
||||
Отформатированная строка со всеми правилами
|
||||
"""
|
||||
manager = get_manager()
|
||||
|
||||
# Получаем все данные из БД
|
||||
try:
|
||||
# Используем существующий метод get_all_words_list()
|
||||
data = await manager.get_all_words_list()
|
||||
stats = await manager.get_stats()
|
||||
|
||||
# Извлекаем данные из словаря
|
||||
permanent_words = list(data.get('substring', set()))
|
||||
permanent_lemmas = list(data.get('lemma', set()))
|
||||
permanent_parts = list(data.get('part', set()))
|
||||
temp_words = list(data.get('temp_substring', set()))
|
||||
temp_lemmas = list(data.get('temp_lemma', set()))
|
||||
conflict_words = list(data.get('conflict_substring', set()))
|
||||
conflict_lemmas = list(data.get('conflict_lemma', set()))
|
||||
exceptions = list(data.get('whitelist', set()))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения данных из БД: {e}", log_type="LISTWORDS")
|
||||
return "❌ <b>Ошибка загрузки данных из базы</b>"
|
||||
|
||||
# === ФОРМИРУЕМ ВЫВОД ===
|
||||
|
||||
output = "📋 <b>СПИСОК ПРАВИЛ МОДЕРАЦИИ</b>\n\n"
|
||||
|
||||
# Статистика
|
||||
total_count = (
|
||||
len(permanent_words) + len(permanent_lemmas) + len(permanent_parts) +
|
||||
len(temp_words) + len(temp_lemmas) +
|
||||
len(conflict_words) + len(conflict_lemmas)
|
||||
)
|
||||
|
||||
output += f"📊 <b>Общая статистика:</b>\n"
|
||||
output += f"├─ Всего правил: <code>{total_count}</code>\n"
|
||||
output += f"├─ Исключений: <code>{len(exceptions)}</code>\n"
|
||||
output += f"├─ Удалений за всё время: <code>{stats.get('total_deletions', 0)}</code>\n"
|
||||
output += f"└─ Администраторов: <code>{stats.get('admins', 0)}</code>\n\n"
|
||||
|
||||
# === ПОСТОЯННЫЕ ПРАВИЛА ===
|
||||
if permanent_words or permanent_lemmas or permanent_parts:
|
||||
output += "🔴 <b>ПОСТОЯННЫЕ ПРАВИЛА:</b>\n\n"
|
||||
|
||||
if permanent_words:
|
||||
output += f"📝 <b>Подстроки</b> ({len(permanent_words)}):\n"
|
||||
words_str = ', '.join([f"<code>{w}</code>" for w in sorted(permanent_words)[:20]])
|
||||
if len(permanent_words) > 20:
|
||||
words_str += f" ... <i>(+{len(permanent_words) - 20} ещё)</i>"
|
||||
output += f"{words_str}\n\n"
|
||||
|
||||
if permanent_lemmas:
|
||||
output += f"🔤 <b>Леммы</b> ({len(permanent_lemmas)}):\n"
|
||||
lemmas_str = ', '.join([f"<code>{w}</code>" for w in sorted(permanent_lemmas)[:20]])
|
||||
if len(permanent_lemmas) > 20:
|
||||
lemmas_str += f" ... <i>(+{len(permanent_lemmas) - 20} ещё)</i>"
|
||||
output += f"{lemmas_str}\n\n"
|
||||
|
||||
if permanent_parts:
|
||||
output += f"🧩 <b>Части</b> ({len(permanent_parts)}):\n"
|
||||
parts_str = ', '.join([f"<code>{w}</code>" for w in sorted(permanent_parts)[:20]])
|
||||
if len(permanent_parts) > 20:
|
||||
parts_str += f" ... <i>(+{len(permanent_parts) - 20} ещё)</i>"
|
||||
output += f"{parts_str}\n\n"
|
||||
|
||||
# === ВРЕМЕННЫЕ ПРАВИЛА ===
|
||||
if temp_words or temp_lemmas:
|
||||
output += "⏱ <b>ВРЕМЕННЫЕ ПРАВИЛА:</b>\n\n"
|
||||
|
||||
if temp_words:
|
||||
output += f"📝 <b>Временные подстроки</b> ({len(temp_words)}):\n"
|
||||
# Для временных слов нужна дополнительная информация о времени истечения
|
||||
# Пока просто выводим список
|
||||
words_str = ', '.join([f"<code>{w}</code>" for w in sorted(temp_words)[:15]])
|
||||
if len(temp_words) > 15:
|
||||
words_str += f" ... <i>(+{len(temp_words) - 15} ещё)</i>"
|
||||
output += f"{words_str}\n\n"
|
||||
|
||||
if temp_lemmas:
|
||||
output += f"🔤 <b>Временные леммы</b> ({len(temp_lemmas)}):\n"
|
||||
lemmas_str = ', '.join([f"<code>{w}</code>" for w in sorted(temp_lemmas)[:15]])
|
||||
if len(temp_lemmas) > 15:
|
||||
lemmas_str += f" ... <i>(+{len(temp_lemmas) - 15} ещё)</i>"
|
||||
output += f"{lemmas_str}\n\n"
|
||||
|
||||
# === КОНФЛИКТНЫЕ ПРАВИЛА ===
|
||||
if conflict_words or conflict_lemmas:
|
||||
output += "⚔️ <b>КОНФЛИКТНЫЕ ПРАВИЛА:</b>\n"
|
||||
output += "<i>(работают только в режиме /stopconflict)</i>\n\n"
|
||||
|
||||
if conflict_words:
|
||||
output += f"📝 <b>Конфликтные слова</b> ({len(conflict_words)}):\n"
|
||||
words_str = ', '.join([f"<code>{w}</code>" for w in sorted(conflict_words)[:15]])
|
||||
if len(conflict_words) > 15:
|
||||
words_str += f" ... <i>(+{len(conflict_words) - 15} ещё)</i>"
|
||||
output += f"{words_str}\n\n"
|
||||
|
||||
if conflict_lemmas:
|
||||
output += f"🔤 <b>Конфликтные леммы</b> ({len(conflict_lemmas)}):\n"
|
||||
lemmas_str = ', '.join([f"<code>{w}</code>" for w in sorted(conflict_lemmas)[:15]])
|
||||
if len(conflict_lemmas) > 15:
|
||||
lemmas_str += f" ... <i>(+{len(conflict_lemmas) - 15} ещё)</i>"
|
||||
output += f"{lemmas_str}\n\n"
|
||||
|
||||
# === ИСКЛЮЧЕНИЯ (WHITELIST) ===
|
||||
if exceptions:
|
||||
output += f"✅ <b>ИСКЛЮЧЕНИЯ</b> ({len(exceptions)}):\n"
|
||||
exc_str = ', '.join([f"<code>{exceptions}</code>" for w in sorted(exceptions)[:15]])
|
||||
if len(exceptions) > 15:
|
||||
exc_str += f" ... <i>(+{len(exceptions) - 15} ещё)</i>"
|
||||
output += f"{exc_str}\n\n"
|
||||
|
||||
# === АКТИВНЫЕ РЕЖИМЫ ===
|
||||
active_modes = []
|
||||
|
||||
if await manager.is_silence_active():
|
||||
active_modes.append("🔇 Режим тишины")
|
||||
|
||||
if await manager.is_conflict_active():
|
||||
active_modes.append("⚔️ Режим антиконфликта")
|
||||
|
||||
if active_modes:
|
||||
output += "🔴 <b>АКТИВНЫЕ РЕЖИМЫ:</b>\n"
|
||||
for mode in active_modes:
|
||||
output += f"{mode}\n"
|
||||
output += "\n"
|
||||
|
||||
# === ПУСТОЙ СПИСОК ===
|
||||
if total_count == 0:
|
||||
output = (
|
||||
"📋 <b>СПИСОК ПРАВИЛ МОДЕРАЦИИ</b>\n\n"
|
||||
"⚠️ <i>Правила модерации не настроены</i>\n\n"
|
||||
"Используйте команды добавления:\n"
|
||||
"• /addword — добавить подстроку\n"
|
||||
"• /addlemma — добавить лемму\n"
|
||||
"• /addpart — добавить часть\n\n"
|
||||
"📖 Подробнее: /start"
|
||||
)
|
||||
|
||||
# Ограничение длины (Telegram limit 4096)
|
||||
if len(output) > 4000:
|
||||
output = output[:3950] + "\n\n<i>... список обрезан, слишком много правил</i>"
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("listwords:refresh"))
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="LISTWORDS_COMMAND")
|
||||
async def listwords_cmd(update: Message | CallbackQuery) -> None:
|
||||
"""
|
||||
Обработчик команды /listwords.
|
||||
Отображает список всех правил модерации с разбивкой по категориям.
|
||||
|
||||
Доступно только администраторам.
|
||||
|
||||
Args:
|
||||
update: Message или CallbackQuery
|
||||
"""
|
||||
# Определяем тип update
|
||||
if isinstance(update, CallbackQuery):
|
||||
message = update.message
|
||||
is_callback = True
|
||||
# Извлекаем номер страницы из callback_data
|
||||
try:
|
||||
page = int(update.data.split(":")[-1])
|
||||
except:
|
||||
page = 0
|
||||
else:
|
||||
message = update
|
||||
is_callback = False
|
||||
page = 0
|
||||
|
||||
# Формируем список
|
||||
try:
|
||||
text = await format_banwords_list(page)
|
||||
keyboard = get_refresh_kb(page)
|
||||
|
||||
if is_callback:
|
||||
await message.edit_text(
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
await update.answer("✅ Список обновлён")
|
||||
else:
|
||||
await message.answer(
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки списка банвордов: {e}", log_type="LISTWORDS")
|
||||
|
||||
error_text = "❌ <b>Ошибка загрузки списка</b>\n\nПопробуйте позже"
|
||||
|
||||
if is_callback:
|
||||
await update.answer("❌ Ошибка загрузки", show_alert=True)
|
||||
else:
|
||||
await message.answer(error_text, parse_mode="HTML")
|
||||
118
bot/handlers/commands/users/notifications.py
Normal file
118
bot/handlers/commands/users/notifications.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
Обработчики callback-кнопок уведомлений о спаме
|
||||
"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import CallbackQuery
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="spam_notifications_router")
|
||||
|
||||
|
||||
# ================= ЗАКРЫТИЕ УВЕДОМЛЕНИЯ =================
|
||||
|
||||
@router.callback_query(F.data == "spam_close", IsAdmin())
|
||||
async def spam_close_callback(callback: CallbackQuery) -> None:
|
||||
"""
|
||||
Закрывает (удаляет) уведомление о спаме.
|
||||
"""
|
||||
try:
|
||||
await callback.message.delete()
|
||||
await callback.answer("✅ Уведомление закрыто")
|
||||
|
||||
logger.debug(
|
||||
f"Уведомление о спаме закрыто админом {callback.from_user.id}",
|
||||
log_type="SPAM_NOTIFICATION"
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
logger.error(f"Ошибка удаления уведомления: {e}", log_type="ERROR")
|
||||
await callback.answer("❌ Не удалось удалить уведомление", show_alert=True)
|
||||
|
||||
|
||||
# ================= БАН ПОЛЬЗОВАТЕЛЯ =================
|
||||
|
||||
@router.callback_query(F.data.startswith("spam_ban:"), IsAdmin())
|
||||
async def spam_ban_callback(callback: CallbackQuery) -> None:
|
||||
"""
|
||||
Банит пользователя прямо из уведомления.
|
||||
"""
|
||||
try:
|
||||
# Парсим данные: spam_ban:user_id:chat_id
|
||||
parts = callback.data.split(":")
|
||||
user_id = int(parts[1])
|
||||
chat_id = int(parts[2])
|
||||
|
||||
# Баним пользователя
|
||||
try:
|
||||
await callback.bot.ban_chat_member(
|
||||
chat_id=chat_id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
# Обновляем сообщение
|
||||
updated_text = callback.message.text + f"\n\n🔨 <b>Пользователь забанен</b> (@{callback.from_user.username or callback.from_user.id})"
|
||||
|
||||
# Убираем кнопки
|
||||
await callback.message.edit_text(
|
||||
text=updated_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer("✅ Пользователь забанен", show_alert=True)
|
||||
|
||||
logger.info(
|
||||
f"Пользователь {user_id} забанен админом {callback.from_user.id} через уведомление о спаме",
|
||||
log_type="SPAM_BAN"
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
await callback.answer(f"❌ Ошибка бана: {str(e)}", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки бана из уведомления: {e}", log_type="ERROR")
|
||||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||||
|
||||
|
||||
# ================= СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ =================
|
||||
|
||||
@router.callback_query(F.data.startswith("spam_stats:"), IsAdmin())
|
||||
async def spam_stats_callback(callback: CallbackQuery) -> None:
|
||||
"""
|
||||
Показывает статистику пользователя.
|
||||
"""
|
||||
try:
|
||||
# Парсим данные: spam_stats:user_id
|
||||
parts = callback.data.split(":")
|
||||
user_id = int(parts[1])
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
# Получаем статистику
|
||||
spam_count = await manager.get_user_spam_count(user_id)
|
||||
recent_spam = await manager.get_spam_stats(limit=5, user_id=user_id)
|
||||
|
||||
# Формируем текст
|
||||
text = f"📊 <b>Статистика пользователя</b>\n\n"
|
||||
text += f"🆔 ID: <code>{user_id}</code>\n"
|
||||
text += f"🗑 Удалено сообщений: <code>{spam_count}</code>\n\n"
|
||||
|
||||
if recent_spam:
|
||||
text += f"📝 <b>Последние нарушения:</b>\n"
|
||||
for idx, stat in enumerate(recent_spam, 1):
|
||||
matched_word = stat.matched_word or "неизвестно"
|
||||
match_type = stat.match_type or "unknown"
|
||||
text += f"{idx}. <code>{matched_word}</code> ({match_type})\n"
|
||||
else:
|
||||
text += "✅ <i>Нет нарушений</i>"
|
||||
|
||||
await callback.answer(text, show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статистики из уведомления: {e}", log_type="ERROR")
|
||||
await callback.answer("❌ Ошибка получения статистики", show_alert=True)
|
||||
447
bot/handlers/commands/users/report.py
Normal file
447
bot/handlers/commands/users/report.py
Normal file
@@ -0,0 +1,447 @@
|
||||
"""
|
||||
Обработчики команды /report для пользователей
|
||||
"""
|
||||
from datetime import datetime
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, User
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="report_router")
|
||||
|
||||
|
||||
# ================= НАСТРОЙКИ =================
|
||||
|
||||
# ID чата для отправки репортов (можно вынести в configs)
|
||||
# Если None, репорты отправляются всем владельцам в ЛС
|
||||
REPORT_CHAT_ID = getattr(settings, 'REPORT_CHAT_ID', None)
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def format_user(user: User) -> str:
|
||||
"""
|
||||
Форматирует информацию о пользователе.
|
||||
|
||||
Args:
|
||||
user: Объект User
|
||||
|
||||
Returns:
|
||||
Отформатированная строка с именем и username
|
||||
"""
|
||||
if not user:
|
||||
return "Unknown User"
|
||||
|
||||
# Формируем имя
|
||||
name_parts = []
|
||||
if user.first_name:
|
||||
name_parts.append(user.first_name)
|
||||
if user.last_name:
|
||||
name_parts.append(user.last_name)
|
||||
|
||||
full_name = " ".join(name_parts) if name_parts else "No Name"
|
||||
|
||||
# Добавляем username если есть
|
||||
if user.username:
|
||||
return f"{full_name} (@{user.username})"
|
||||
else:
|
||||
return full_name
|
||||
|
||||
|
||||
def format_datetime(dt: datetime) -> str:
|
||||
"""Форматирует datetime"""
|
||||
return dt.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
|
||||
def truncate_text(text: str, max_length: int = 200) -> str:
|
||||
"""Обрезает текст до указанной длины"""
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
return text[:max_length] + "..."
|
||||
|
||||
|
||||
def get_report_keyboard(
|
||||
chat_id: int,
|
||||
message_id: int,
|
||||
reported_user_id: int,
|
||||
report_id: str
|
||||
) -> InlineKeyboardBuilder:
|
||||
"""
|
||||
Создает клавиатуру для репорта.
|
||||
|
||||
Args:
|
||||
chat_id: ID чата, где было сообщение
|
||||
message_id: ID сообщения
|
||||
reported_user_id: ID пользователя, на которого пожаловались
|
||||
report_id: Уникальный ID репорта
|
||||
"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
|
||||
# Кнопки действий
|
||||
ikb.button(
|
||||
text="🚫 Забанить",
|
||||
callback_data=f"report:ban:{chat_id}:{reported_user_id}:{report_id}"
|
||||
)
|
||||
ikb.button(
|
||||
text="🗑 Удалить",
|
||||
callback_data=f"report:delete:{chat_id}:{message_id}:{report_id}"
|
||||
)
|
||||
ikb.button(
|
||||
text="✅ Закрыть",
|
||||
callback_data=f"report:close:{report_id}"
|
||||
)
|
||||
|
||||
ikb.adjust(2, 1)
|
||||
return ikb
|
||||
|
||||
|
||||
def generate_report_id() -> str:
|
||||
"""Генерирует уникальный ID репорта"""
|
||||
return f"{int(datetime.now().timestamp() * 1000)}"
|
||||
|
||||
|
||||
# ================= КОМАНДА РЕПОРТА =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("report", ["report"]), prefix=settings.PREFIX, ignore_case=True))
|
||||
async def report_cmd(message: Message) -> None:
|
||||
"""
|
||||
Отправляет жалобу на сообщение администраторам.
|
||||
|
||||
Доступно всем пользователям.
|
||||
|
||||
Использование:
|
||||
/report — в ответ на сообщение
|
||||
/report <причина> — в ответ на сообщение с указанием причины
|
||||
|
||||
Пример:
|
||||
/report спам
|
||||
/report оскорбления
|
||||
"""
|
||||
# Проверяем, что команда в ответ на сообщение
|
||||
if not message.reply_to_message:
|
||||
await message.answer(
|
||||
"❌ <b>Используйте команду в ответ на сообщение</b>\n\n"
|
||||
"Как использовать:\n"
|
||||
"1. Ответьте на сообщение нарушителя\n"
|
||||
"2. Напишите <code>/report</code> или <code>/report причина</code>\n\n"
|
||||
"Пример: <code>/report спам</code>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
reported_message = message.reply_to_message
|
||||
reported_user = reported_message.from_user
|
||||
reporter = message.from_user
|
||||
|
||||
# Проверка на None
|
||||
if not reported_user or not reporter:
|
||||
await message.answer("❌ <b>Ошибка получения данных пользователя</b>", parse_mode="HTML")
|
||||
return
|
||||
|
||||
# Нельзя пожаловаться на самого себя
|
||||
if reported_user.id == reporter.id:
|
||||
await message.answer(
|
||||
"⚠️ <b>Нельзя пожаловаться на самого себя</b>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Нельзя пожаловаться на бота
|
||||
if reported_user.is_bot:
|
||||
await message.answer(
|
||||
"⚠️ <b>Нельзя пожаловаться на бота</b>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Нельзя пожаловаться на администратора
|
||||
manager = get_manager()
|
||||
is_admin = await manager.is_admin(reported_user.id) or reported_user.id in settings.OWNER_ID
|
||||
|
||||
if is_admin:
|
||||
await message.answer(
|
||||
"⚠️ <b>Нельзя пожаловаться на администратора</b>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Извлекаем причину (опционально)
|
||||
parts = message.text.split(maxsplit=1)
|
||||
reason = parts[1] if len(parts) > 1 else "Не указана"
|
||||
|
||||
# Генерируем ID репорта
|
||||
report_id = generate_report_id()
|
||||
|
||||
# === ФОРМИРУЕМ СООБЩЕНИЕ РЕПОРТА ===
|
||||
|
||||
report_text = "🚨 <b>НОВЫЙ РЕПОРТ</b>\n\n"
|
||||
|
||||
# Информация о жалобщике
|
||||
report_text += f"👤 <b>От:</b> {format_user(reporter)} (<code>{reporter.id}</code>)\n"
|
||||
|
||||
# Информация о нарушителе
|
||||
report_text += f"⚠️ <b>На:</b> {format_user(reported_user)} (<code>{reported_user.id}</code>)\n\n"
|
||||
|
||||
# Информация о чате
|
||||
chat_title = message.chat.title if message.chat.title else "Личные сообщения"
|
||||
report_text += f"💬 <b>Чат:</b> {chat_title}\n"
|
||||
report_text += f"🆔 <b>Chat ID:</b> <code>{message.chat.id}</code>\n\n"
|
||||
|
||||
# Причина
|
||||
report_text += f"📝 <b>Причина:</b> {reason}\n\n"
|
||||
|
||||
# Текст сообщения
|
||||
report_text += f"📄 <b>Текст сообщения:</b>\n"
|
||||
|
||||
if reported_message.text:
|
||||
truncated_text = truncate_text(reported_message.text, max_length=300)
|
||||
report_text += f"<code>{truncated_text}</code>\n\n"
|
||||
elif reported_message.caption:
|
||||
truncated_caption = truncate_text(reported_message.caption, max_length=300)
|
||||
report_text += f"<code>{truncated_caption}</code>\n\n"
|
||||
else:
|
||||
content_type = reported_message.content_type
|
||||
report_text += f"<i>[{content_type}]</i>\n\n"
|
||||
|
||||
# Время
|
||||
report_text += f"🕐 <b>Время:</b> {format_datetime(datetime.now())}\n"
|
||||
report_text += f"🔗 <b>Message ID:</b> <code>{reported_message.message_id}</code>\n\n"
|
||||
|
||||
report_text += f"💡 <i>ID репорта: {report_id}</i>"
|
||||
|
||||
# Клавиатура
|
||||
keyboard = get_report_keyboard(
|
||||
chat_id=message.chat.id,
|
||||
message_id=reported_message.message_id,
|
||||
reported_user_id=reported_user.id,
|
||||
report_id=report_id
|
||||
)
|
||||
|
||||
# === ОТПРАВКА РЕПОРТА ===
|
||||
|
||||
try:
|
||||
# Если указан админ-чат, отправляем туда
|
||||
if REPORT_CHAT_ID:
|
||||
await message.bot.send_message(
|
||||
chat_id=REPORT_CHAT_ID,
|
||||
text=report_text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard.as_markup()
|
||||
)
|
||||
else:
|
||||
# Отправляем всем владельцам
|
||||
sent_count = 0
|
||||
for owner_id in settings.OWNER_ID:
|
||||
try:
|
||||
await message.bot.send_message(
|
||||
chat_id=owner_id,
|
||||
text=report_text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard.as_markup()
|
||||
)
|
||||
sent_count += 1
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки репорта владельцу {owner_id}: {e}", log_type="REPORT")
|
||||
|
||||
if sent_count == 0:
|
||||
raise Exception("Не удалось отправить репорт ни одному владельцу")
|
||||
|
||||
# Подтверждение пользователю
|
||||
await message.answer(
|
||||
"✅ <b>Жалоба отправлена администраторам</b>\n\n"
|
||||
"Спасибо за бдительность! Администраторы рассмотрят вашу жалобу.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Логирование
|
||||
logger.info(
|
||||
f"Репорт #{report_id}: {reporter.id} → {reported_user.id} в чате {message.chat.id}",
|
||||
log_type="REPORT"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отправки репорта: {e}", log_type="REPORT")
|
||||
await message.answer(
|
||||
"❌ <b>Ошибка отправки жалобы</b>\n\nПопробуйте позже или обратитесь к администратору напрямую.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
|
||||
# ================= ОБРАБОТЧИКИ КНОПОК =================
|
||||
|
||||
@router.callback_query(F.data.startswith("report:ban:"), IsAdmin())
|
||||
async def report_ban_callback(callback: CallbackQuery) -> None:
|
||||
"""Обрабатывает нажатие кнопки 'Забанить'"""
|
||||
try:
|
||||
# Парсим данные: report:ban:chat_id:user_id:report_id
|
||||
parts = callback.data.split(":")
|
||||
chat_id = int(parts[2])
|
||||
user_id = int(parts[3])
|
||||
report_id = parts[4]
|
||||
|
||||
# Баним пользователя
|
||||
try:
|
||||
await callback.bot.ban_chat_member(
|
||||
chat_id=chat_id,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
admin_name = format_user(callback.from_user)
|
||||
|
||||
# Обновляем сообщение
|
||||
updated_text = callback.message.text + f"\n\n✅ <b>Пользователь забанен</b> ({admin_name})"
|
||||
|
||||
# Убираем кнопки
|
||||
await callback.message.edit_text(
|
||||
text=updated_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer("✅ Пользователь забанен", show_alert=True)
|
||||
|
||||
logger.info(
|
||||
f"Репорт #{report_id}: пользователь {user_id} забанен админом {callback.from_user.id}",
|
||||
log_type="REPORT"
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
await callback.answer(f"❌ Ошибка бана: {str(e)}", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки бана из репорта: {e}", log_type="REPORT")
|
||||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("report:delete:"), IsAdmin())
|
||||
async def report_delete_callback(callback: CallbackQuery) -> None:
|
||||
"""Обрабатывает нажатие кнопки 'Удалить'"""
|
||||
try:
|
||||
# Парсим данные: report:delete:chat_id:message_id:report_id
|
||||
parts = callback.data.split(":")
|
||||
chat_id = int(parts[2])
|
||||
message_id = int(parts[3])
|
||||
report_id = parts[4]
|
||||
|
||||
# Удаляем сообщение
|
||||
try:
|
||||
await callback.bot.delete_message(
|
||||
chat_id=chat_id,
|
||||
message_id=message_id
|
||||
)
|
||||
|
||||
admin_name = format_user(callback.from_user)
|
||||
|
||||
# Обновляем сообщение
|
||||
updated_text = callback.message.text + f"\n\n🗑 <b>Сообщение удалено</b> ({admin_name})"
|
||||
|
||||
# Убираем кнопки
|
||||
await callback.message.edit_text(
|
||||
text=updated_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer("✅ Сообщение удалено", show_alert=True)
|
||||
|
||||
logger.info(
|
||||
f"Репорт #{report_id}: сообщение {message_id} удалено админом {callback.from_user.id}",
|
||||
log_type="REPORT"
|
||||
)
|
||||
|
||||
except TelegramBadRequest as e:
|
||||
await callback.answer(f"❌ Ошибка удаления: {str(e)}", show_alert=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления из репорта: {e}", log_type="REPORT")
|
||||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("report:close:"), IsAdmin())
|
||||
async def report_close_callback(callback: CallbackQuery) -> None:
|
||||
"""Обрабатывает нажатие кнопки 'Закрыть'"""
|
||||
try:
|
||||
# Парсим данные: report:close:report_id
|
||||
parts = callback.data.split(":")
|
||||
report_id = parts[2]
|
||||
|
||||
admin_name = format_user(callback.from_user)
|
||||
|
||||
# Обновляем сообщение
|
||||
updated_text = callback.message.text + f"\n\n✅ <b>Репорт закрыт</b> ({admin_name})"
|
||||
|
||||
# Убираем кнопки
|
||||
await callback.message.edit_text(
|
||||
text=updated_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
await callback.answer("✅ Репорт закрыт")
|
||||
|
||||
logger.info(
|
||||
f"Репорт #{report_id} закрыт админом {callback.from_user.id}",
|
||||
log_type="REPORT"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка закрытия репорта: {e}", log_type="REPORT")
|
||||
await callback.answer("❌ Ошибка выполнения", show_alert=True)
|
||||
|
||||
|
||||
# ================= ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("reporthelp", ["reporthelp"]), prefix=settings.PREFIX, ignore_case=True))
|
||||
async def report_help_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает справку по системе репортов.
|
||||
|
||||
Доступно всем пользователям.
|
||||
"""
|
||||
text = (
|
||||
"🚨 <b>СИСТЕМА РЕПОРТОВ</b>\n\n"
|
||||
"Используйте команду /report, чтобы пожаловаться на сообщение администраторам.\n\n"
|
||||
"📝 <b>Как пожаловаться:</b>\n"
|
||||
"1. Ответьте на сообщение нарушителя\n"
|
||||
"2. Напишите <code>/report</code>\n"
|
||||
"3. Можно указать причину: <code>/report спам</code>\n\n"
|
||||
"✅ <b>Примеры:</b>\n"
|
||||
"• <code>/report</code> — жалоба без причины\n"
|
||||
"• <code>/report спам</code> — жалоба на спам\n"
|
||||
"• <code>/report оскорбления</code> — жалоба на оскорбления\n\n"
|
||||
"⚠️ <b>Важно:</b>\n"
|
||||
"├─ Нельзя пожаловаться на себя\n"
|
||||
"├─ Нельзя пожаловаться на ботов\n"
|
||||
"├─ Нельзя пожаловаться на администраторов\n"
|
||||
"└─ Ложные жалобы могут привести к бану\n\n"
|
||||
"💡 <i>Администраторы получат уведомление и примут меры</i>"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("reportstats", ["reportstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
async def report_stats_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает статистику по репортам (для админов).
|
||||
|
||||
TODO: Реализовать сохранение статистики в БД
|
||||
"""
|
||||
text = (
|
||||
"📊 <b>СТАТИСТИКА РЕПОРТОВ</b>\n\n"
|
||||
"⚠️ <i>Функция в разработке</i>\n\n"
|
||||
"Планируется:\n"
|
||||
"• Всего репортов за всё время\n"
|
||||
"• Топ жалобщиков\n"
|
||||
"• Топ нарушителей\n"
|
||||
"• Распределение по причинам\n"
|
||||
"• Статистика обработки\n\n"
|
||||
"💡 <i>Для реализации нужно добавить таблицу reports в БД</i>"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
346
bot/handlers/commands/users/slience.py
Normal file
346
bot/handlers/commands/users/slience.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Обработчики команд режима тишины
|
||||
"""
|
||||
from datetime import datetime
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="silence_mode_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def parse_silence_args(text: str) -> tuple[bool, str | int]:
|
||||
"""
|
||||
Парсит аргументы команды для режима тишины.
|
||||
|
||||
Args:
|
||||
text: Полный текст сообщения
|
||||
|
||||
Returns:
|
||||
(success, result): result это либо минуты (int), либо текст ошибки (str)
|
||||
"""
|
||||
parts = text.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
return False, "❌ Использование: <code>/silence <минуты></code>"
|
||||
|
||||
return True, parts[1]
|
||||
|
||||
|
||||
def format_time_str(minutes: int) -> str:
|
||||
"""Форматирует время в читабельный формат"""
|
||||
if minutes < 60:
|
||||
return f"{minutes} мин"
|
||||
elif minutes < 1440:
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
return f"{hours}ч {mins}м" if mins else f"{hours}ч"
|
||||
else:
|
||||
days = minutes // 1440
|
||||
hours = (minutes % 1440) // 60
|
||||
return f"{days}д {hours}ч" if hours else f"{days}д"
|
||||
|
||||
|
||||
def format_datetime(dt: datetime) -> str:
|
||||
"""Форматирует datetime в читабельный формат"""
|
||||
return dt.strftime("%d.%m.%Y %H:%M:%S")
|
||||
|
||||
|
||||
# ================= КОМАНДЫ РЕЖИМА ТИШИНЫ =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("silence", ["silence"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="START_SILENCE_MODE", log_args=True)
|
||||
async def start_silence_mode_cmd(message: Message) -> None:
|
||||
"""
|
||||
Активирует режим тишины на указанное время.
|
||||
|
||||
В этом режиме удаляются ВСЕ сообщения от обычных пользователей.
|
||||
Администраторы могут продолжать писать.
|
||||
|
||||
Использование: /silence <минуты>
|
||||
Примеры:
|
||||
/silence 30 — на 30 минут
|
||||
/silence 120 — на 2 часа
|
||||
/silence 1440 — на сутки
|
||||
"""
|
||||
success, result = parse_silence_args(message.text)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
# Валидация минут
|
||||
try:
|
||||
minutes = int(result)
|
||||
if minutes < 1 or minutes > 10080: # Максимум неделя
|
||||
await message.answer(
|
||||
"❌ Время должно быть от 1 минуты до 10080 минут (7 дней)",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем, уже активен ли режим
|
||||
is_already_active = await manager.is_silence_active()
|
||||
|
||||
# Активируем режим (перезаписывает предыдущий, если был)
|
||||
expires_at = await manager.set_silence_mode(minutes)
|
||||
|
||||
time_str = format_time_str(minutes)
|
||||
expires_str = format_datetime(expires_at)
|
||||
|
||||
if is_already_active:
|
||||
action_text = "🔄 <b>РЕЖИМ ТИШИНЫ ОБНОВЛЁН</b>"
|
||||
else:
|
||||
action_text = "🔇 <b>РЕЖИМ ТИШИНЫ АКТИВИРОВАН</b>"
|
||||
|
||||
text = (
|
||||
f"{action_text}\n\n"
|
||||
f"⏱ Длительность: {time_str}\n"
|
||||
f"🕐 Окончание: {expires_str}\n\n"
|
||||
f"⚠️ <b>Что происходит:</b>\n"
|
||||
f"├─ Все сообщения от пользователей удаляются\n"
|
||||
f"├─ Администраторы могут писать\n"
|
||||
f"└─ Банворды временно отключены\n\n"
|
||||
f"💡 <i>Используйте для успокоения спора или флуда</i>\n"
|
||||
f"Отключить досрочно: /unsilence"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
logger.info(
|
||||
f"Режим тишины {'обновлён' if is_already_active else 'активирован'} на {minutes} мин "
|
||||
f"пользователем {message.from_user.id}",
|
||||
log_type="SILENCE"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка активации режима тишины: {e}", log_type="SILENCE")
|
||||
await message.answer("❌ <b>Ошибка активации режима</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("unsilence", ["unsilence"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="STOP_SILENCE_MODE")
|
||||
async def stop_silence_mode_cmd(message: Message) -> None:
|
||||
"""
|
||||
Отключает режим тишины.
|
||||
|
||||
Использование: /unsilence
|
||||
"""
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем, активен ли режим
|
||||
is_active = await manager.is_silence_active()
|
||||
|
||||
if not is_active:
|
||||
await message.answer(
|
||||
"⚠️ <b>Режим тишины не активен</b>\n\n"
|
||||
"Активируйте командой: /silence <минуты>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Отключаем режим
|
||||
await manager.disable_silence_mode()
|
||||
|
||||
text = (
|
||||
f"✅ <b>Режим тишины отключен</b>\n\n"
|
||||
f"🔊 Пользователи снова могут отправлять сообщения\n"
|
||||
f"🔄 Банворды снова активны"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
logger.info(
|
||||
f"Режим тишины отключён пользователем {message.from_user.id}",
|
||||
log_type="SILENCE"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка отключения режима тишины: {e}", log_type="SILENCE")
|
||||
await message.answer("❌ <b>Ошибка отключения режима</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("silencestatus", ["silencestatus"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="SILENCE_STATUS")
|
||||
async def silence_status_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает статус режима тишины.
|
||||
|
||||
Использование: /silencestatus
|
||||
"""
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Проверяем активность режима
|
||||
is_active = await manager.is_silence_active()
|
||||
|
||||
if is_active:
|
||||
# Режим активен - показываем детали
|
||||
silence_until_str = await manager.repo.get_setting("silence_until")
|
||||
silence_until = float(silence_until_str)
|
||||
expires_at = datetime.fromtimestamp(silence_until)
|
||||
|
||||
now = datetime.now()
|
||||
time_left_seconds = (expires_at - now).total_seconds()
|
||||
time_left_minutes = int(time_left_seconds / 60)
|
||||
|
||||
# Расчёт процента прошедшего времени (для визуализации)
|
||||
# Примерно определяем начальное время
|
||||
started_minutes_ago = 0 # Можно было бы сохранять в БД
|
||||
|
||||
text = (
|
||||
f"🔇 <b>РЕЖИМ ТИШИНЫ АКТИВЕН</b>\n\n"
|
||||
f"⏱ Осталось: {format_time_str(time_left_minutes)}\n"
|
||||
f"🕐 Окончание: {format_datetime(expires_at)}\n\n"
|
||||
f"⚠️ <b>Что происходит:</b>\n"
|
||||
f"├─ Все сообщения от пользователей удаляются\n"
|
||||
f"├─ Администраторы могут писать\n"
|
||||
f"└─ Банворды временно отключены\n\n"
|
||||
f"💡 <i>Для успокоения конфликта или флуда</i>\n"
|
||||
f"Отключить: /unsilence"
|
||||
)
|
||||
|
||||
# Добавляем визуальную шкалу прогресса
|
||||
if time_left_minutes <= 60:
|
||||
progress_bar = create_progress_bar(time_left_minutes, 60)
|
||||
text += f"\n\n{progress_bar}"
|
||||
|
||||
else:
|
||||
# Режим не активен
|
||||
text = (
|
||||
f"💤 <b>Режим тишины НЕ активен</b>\n\n"
|
||||
f"🔊 Пользователи могут отправлять сообщения\n"
|
||||
f"🔄 Банворды работают в обычном режиме\n\n"
|
||||
f"Активировать: /silence <минуты>"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статуса режима тишины: {e}", log_type="SILENCE")
|
||||
await message.answer("❌ <b>Ошибка получения статуса</b>", parse_mode="HTML")
|
||||
|
||||
|
||||
def create_progress_bar(minutes_left: int, total_minutes: int, length: int = 10) -> str:
|
||||
"""
|
||||
Создает визуальную шкалу прогресса.
|
||||
|
||||
Args:
|
||||
minutes_left: Сколько минут осталось
|
||||
total_minutes: Всего минут
|
||||
length: Длина шкалы
|
||||
|
||||
Returns:
|
||||
Строка с визуальной шкалой
|
||||
"""
|
||||
if total_minutes <= 0:
|
||||
filled = 0
|
||||
else:
|
||||
filled = int((total_minutes - minutes_left) / total_minutes * length)
|
||||
|
||||
filled = max(0, min(filled, length))
|
||||
empty = length - filled
|
||||
|
||||
bar = "█" * filled + "░" * empty
|
||||
percentage = int((total_minutes - minutes_left) / total_minutes * 100) if total_minutes > 0 else 0
|
||||
|
||||
return f"[{bar}] {percentage}%"
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("extend_silence", ["extend_silence"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="EXTEND_SILENCE_MODE", log_args=True)
|
||||
async def extend_silence_mode_cmd(message: Message) -> None:
|
||||
"""
|
||||
Продлевает режим тишины на указанное время.
|
||||
|
||||
Использование: /extend_silence <минуты>
|
||||
Пример: /extend_silence 30
|
||||
"""
|
||||
success, result = parse_silence_args(message.text)
|
||||
|
||||
if not success:
|
||||
# Меняем текст ошибки для extend команды
|
||||
await message.answer(
|
||||
"❌ Использование: <code>/extend_silence <минуты></code>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
# Проверяем, активен ли режим
|
||||
manager = get_manager()
|
||||
is_active = await manager.is_silence_active()
|
||||
|
||||
if not is_active:
|
||||
await message.answer(
|
||||
"⚠️ <b>Режим тишины не активен</b>\n\n"
|
||||
"Сначала активируйте: /silence <минуты>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
add_minutes = int(result)
|
||||
if add_minutes < 1 or add_minutes > 1440:
|
||||
await message.answer(
|
||||
"❌ Время продления должно быть от 1 до 1440 минут (24 часа)",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем текущее время окончания
|
||||
silence_until_str = await manager.repo.get_setting("silence_until")
|
||||
current_until = float(silence_until_str)
|
||||
current_expires = datetime.fromtimestamp(current_until)
|
||||
|
||||
# Вычисляем сколько минут осталось + добавляем новые
|
||||
now = datetime.now()
|
||||
current_minutes_left = int((current_expires - now).total_seconds() / 60)
|
||||
new_total_minutes = current_minutes_left + add_minutes
|
||||
|
||||
# Устанавливаем новое время
|
||||
new_expires_at = await manager.set_silence_mode(new_total_minutes)
|
||||
|
||||
time_str = format_time_str(add_minutes)
|
||||
new_expires_str = format_datetime(new_expires_at)
|
||||
|
||||
text = (
|
||||
f"⏱ <b>РЕЖИМ ТИШИНЫ ПРОДЛЁН</b>\n\n"
|
||||
f"➕ Добавлено: {time_str}\n"
|
||||
f"🕐 Новое окончание: {new_expires_str}\n"
|
||||
f"⏳ Всего осталось: {format_time_str(new_total_minutes)}\n\n"
|
||||
f"Отключить: /unsilence"
|
||||
)
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
logger.info(
|
||||
f"Режим тишины продлён на {add_minutes} мин (всего: {new_total_minutes} мин)",
|
||||
log_type="SILENCE"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка продления режима тишины: {e}", log_type="SILENCE")
|
||||
await message.answer("❌ <b>Ошибка продления режима</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
168
bot/handlers/commands/users/start_cmd.py
Normal file
168
bot/handlers/commands/users/start_cmd.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Обработчик команды /start и /help для администраторов.
|
||||
Показывает список доступных команд для управления банвордами.
|
||||
"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "start"
|
||||
router: Router = Router(name="start_cmd_router")
|
||||
|
||||
def kb(text: str = "Создатель⬆️", url: str = "https://t.me/verdise"):
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text=text, url=url)
|
||||
return ikb.as_markup()
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="START_COMMAND", log_args=True)
|
||||
async def start_cmd(update: Message | CallbackQuery) -> None:
|
||||
"""
|
||||
Обработчик команды /start и /help.
|
||||
Показывает справку по командам бота для администраторов.
|
||||
|
||||
Доступно только администраторам (суперадмин или доп. админ из БД).
|
||||
|
||||
Args:
|
||||
update: Message или CallbackQuery
|
||||
"""
|
||||
print(123)
|
||||
# Определяем тип update и извлекаем данные
|
||||
if isinstance(update, CallbackQuery):
|
||||
message = update.message
|
||||
user_id = update.from_user.id
|
||||
is_callback = True
|
||||
else:
|
||||
message = update
|
||||
user_id = update.from_user.id
|
||||
is_callback = False
|
||||
|
||||
# Проверяем, является ли пользователь суперадмином
|
||||
is_super_admin = user_id in settings.OWNER_ID
|
||||
|
||||
# Формируем текст помощи
|
||||
help_text = (
|
||||
"🤖 <b>PrimoGuard - Бот-модератор</b>\n\n"
|
||||
"Автоматическое удаление сообщений с запрещёнными словами.\n"
|
||||
"Поддержка подстрок, лемм, временных блокировок и режимов модерации.\n\n"
|
||||
)
|
||||
|
||||
# === Команды просмотра ===
|
||||
help_text += (
|
||||
"📋 <b>Просмотр:</b>\n"
|
||||
"/list — список всех правил и слов\n"
|
||||
"/stats — статистика по удалениям\n"
|
||||
"/id — получение айди пользователя\n"
|
||||
"/chatid — получение айди чата\n\n"
|
||||
)
|
||||
|
||||
# === Постоянные банворды ===
|
||||
help_text += (
|
||||
"➕ <b>Добавить банворд (постоянно):</b>\n"
|
||||
"/addword <code>слово</code> — подстрока (простой поиск)\n"
|
||||
"/addlemma <code>слово</code> — лемма (все формы слова)\n"
|
||||
"/addpart <code>комбинация</code> — часть (поиск без пробелов)\n\n"
|
||||
)
|
||||
|
||||
# === Временные банворды ===
|
||||
help_text += (
|
||||
"⏱ <b>Добавить банворд (временно):</b>\n"
|
||||
"/addtempword <code>слово минуты</code> — временная подстрока\n"
|
||||
"/addtemplemma <code>слово минуты</code> — временная лемма\n"
|
||||
"<i>Пример: /addtempword спам 60</i>\n\n"
|
||||
)
|
||||
|
||||
# === Исключения (whitelist) ===
|
||||
help_text += (
|
||||
"✅ <b>Исключения (whitelist):</b>\n"
|
||||
"/addexcept <code>текст</code> — добавить исключение\n"
|
||||
"/remexcept <code>текст</code> — удалить исключение\n"
|
||||
"<i>Исключения не проверяются фильтром</i>\n\n"
|
||||
)
|
||||
|
||||
# === Режимы модерации ===
|
||||
help_text += (
|
||||
"🔇 <b>Режим тишины:</b>\n"
|
||||
"/silence <code>минуты</code> — удалять ВСЕ сообщения\n"
|
||||
"/unsilence — отключить режим тишины\n\n"
|
||||
)
|
||||
|
||||
help_text += (
|
||||
"⚔️ <b>Режим антиконфликта:</b>\n"
|
||||
"/addconflictword <code>слово</code> — добавить конфликтное слово\n"
|
||||
"/addconflictlemma <code>слово</code> — добавить конфликтную лемму\n"
|
||||
"/stopconflict <code>минуты</code> — активировать режим\n"
|
||||
"/unstopconflict — отключить режим\n\n"
|
||||
)
|
||||
|
||||
# === Удаление ===
|
||||
help_text += (
|
||||
"➖ <b>Удалить:</b>\n"
|
||||
"/remword <code>слово</code> — удалить подстроку\n"
|
||||
"/remlemma <code>слово</code> — удалить лемму\n"
|
||||
"/rempart <code>комбинация</code> — удалить часть\n"
|
||||
"/remtempword <code>слово</code> — удалить временную подстроку\n"
|
||||
"/remtemplemma <code>слово</code> — удалить временную лемму\n"
|
||||
"/remconflictword <code>слово</code> — удалить конфликтное слово\n"
|
||||
"/remconflictlemma <code>слово</code> — удалить конфликтную лемму\n\n"
|
||||
)
|
||||
|
||||
# === Управление админами (только для суперадминов) ===
|
||||
if is_super_admin:
|
||||
help_text += (
|
||||
"👑 <b>Управление админами (только для владельцев):</b>\n"
|
||||
"/addadmin <code>ID</code> — добавить администратора\n"
|
||||
"/remadmin <code>ID</code> — удалить администратора\n"
|
||||
"/listadmins — список всех админов\n\n"
|
||||
)
|
||||
|
||||
# === Типы проверок ===
|
||||
help_text += (
|
||||
"ℹ️ <b>Типы проверок:</b>\n"
|
||||
"• <b>Подстрока</b> — простой поиск в тексте\n"
|
||||
"• <b>Лемма</b> — все формы слова (купить→куплю, купил, купишь...)\n"
|
||||
"• <b>Часть</b> — поиск без пробелов (обходит \"к у п и т ь\")\n"
|
||||
"• <b>Временные</b> — автоматически удаляются через N минут\n"
|
||||
"• <b>Конфликтные</b> — работают только в режиме /stopconflict\n\n"
|
||||
)
|
||||
|
||||
help_text += (
|
||||
"🔧 <b>Технологии:</b>\n"
|
||||
"• Unicode-нормализация (латиница→кириллица)\n"
|
||||
"• Обход через разделители (\"с п а м\" → \"спам\")\n"
|
||||
"• Морфологический анализ (pymorphy3)\n"
|
||||
"• SQLAlchemy + SQLite с кэшированием\n\n"
|
||||
"💾 Все настройки сохраняются в базе данных"
|
||||
)
|
||||
|
||||
# Отправляем ответ
|
||||
try:
|
||||
if is_callback:
|
||||
await message.edit_text(
|
||||
text=help_text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=kb()
|
||||
)
|
||||
await update.answer()
|
||||
else:
|
||||
await message.answer(
|
||||
text=help_text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=kb()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка отправки help сообщения: {e}",
|
||||
log_type="ERROR"
|
||||
)
|
||||
if is_callback:
|
||||
await update.answer("❌ Ошибка отображения справки", show_alert=True)
|
||||
589
bot/handlers/commands/users/stats.py
Normal file
589
bot/handlers/commands/users/stats.py
Normal file
@@ -0,0 +1,589 @@
|
||||
"""
|
||||
Обработчики команды статистики
|
||||
"""
|
||||
from datetime import datetime
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="stats_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def format_number(num: int) -> str:
|
||||
"""Форматирует большие числа с разделителями"""
|
||||
return f"{num:,}".replace(",", " ")
|
||||
|
||||
|
||||
def create_text_bar(value: int, max_value: int, length: int = 10) -> str:
|
||||
"""Создает текстовую полоску прогресса"""
|
||||
if max_value == 0:
|
||||
return "░" * length
|
||||
|
||||
filled = int((value / max_value) * length)
|
||||
filled = max(0, min(filled, length))
|
||||
empty = length - filled
|
||||
|
||||
return "█" * filled + "░" * empty
|
||||
|
||||
|
||||
def format_datetime(dt: datetime) -> str:
|
||||
"""Форматирует datetime в читабельный формат"""
|
||||
return dt.strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
|
||||
def format_time_remaining(minutes: int) -> str:
|
||||
"""
|
||||
Форматирует оставшееся время в читабельный формат.
|
||||
|
||||
Args:
|
||||
minutes: Количество минут
|
||||
|
||||
Returns:
|
||||
Отформатированная строка времени
|
||||
"""
|
||||
if minutes <= 0:
|
||||
return "истёк"
|
||||
elif minutes < 60:
|
||||
return f"{minutes} мин"
|
||||
elif minutes < 1440: # < 24 часов
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
if mins > 0:
|
||||
return f"{hours}ч {mins}м"
|
||||
return f"{hours}ч"
|
||||
else: # >= 24 часов
|
||||
days = minutes // 1440
|
||||
hours = (minutes % 1440) // 60
|
||||
if hours > 0:
|
||||
return f"{days}д {hours}ч"
|
||||
return f"{days}д"
|
||||
|
||||
|
||||
def get_stats_keyboard():
|
||||
"""Клавиатура для статистики"""
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="🔄 Обновить", callback_data="stats:refresh")
|
||||
ikb.button(text="📊 Детали", callback_data="stats:details")
|
||||
ikb.button(text="🏆 Топ-спамеры", callback_data="stats:top_spammers")
|
||||
ikb.button(text="🔤 Топ-слова", callback_data="stats:top_words")
|
||||
ikb.button(text="🚀 Назад", callback_data="start")
|
||||
ikb.adjust(2, 2, 1)
|
||||
return ikb.as_markup()
|
||||
|
||||
|
||||
# ================= ОСНОВНАЯ СТАТИСТИКА =================
|
||||
|
||||
@router.callback_query(F.data == "stats:refresh")
|
||||
@router.callback_query(F.data == "stats")
|
||||
@router.message(Command(*COMMANDS.get("stats", ["stats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="VIEW_STATS")
|
||||
async def stats_cmd(update: Message | CallbackQuery) -> None:
|
||||
"""
|
||||
Показывает общую статистику работы бота.
|
||||
|
||||
Включает:
|
||||
- Общее количество удалений
|
||||
- Активные режимы
|
||||
- Статистику банвордов
|
||||
- Топ спамеров
|
||||
|
||||
Использование: /stats
|
||||
"""
|
||||
# Определяем тип update
|
||||
if isinstance(update, CallbackQuery):
|
||||
message = update.message
|
||||
is_callback = True
|
||||
else:
|
||||
message = update
|
||||
is_callback = False
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Получаем данные
|
||||
stats = await manager.get_stats()
|
||||
data = await manager.get_all_words_list()
|
||||
top_spammers = await manager.get_top_spammers(limit=5)
|
||||
|
||||
# Проверяем активные режимы
|
||||
is_silence = await manager.is_silence_active()
|
||||
is_conflict = await manager.is_conflict_active()
|
||||
|
||||
# === ФОРМИРУЕМ ВЫВОД ===
|
||||
|
||||
output = "📊 <b>СТАТИСТИКА PRIMOGUARD</b>\n\n"
|
||||
|
||||
# Общая информация
|
||||
total_deletions = stats.get('total_deletions', 0)
|
||||
output += f"🗑 <b>Всего удалений:</b> <code>{format_number(total_deletions)}</code>\n\n"
|
||||
|
||||
# Активные режимы
|
||||
if is_silence or is_conflict:
|
||||
output += "🔴 <b>АКТИВНЫЕ РЕЖИМЫ:</b>\n\n"
|
||||
|
||||
if is_silence:
|
||||
silence_until_str = await manager.repo.get_setting("silence_until")
|
||||
silence_until = datetime.fromtimestamp(float(silence_until_str))
|
||||
time_left_seconds = (silence_until - datetime.now()).total_seconds()
|
||||
time_left_minutes = int(time_left_seconds / 60)
|
||||
|
||||
output += f"🔇 <b>Режим тишины</b>\n"
|
||||
output += f"├─ ⏱ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||||
output += f"└─ 🕐 До: {format_datetime(silence_until)}\n"
|
||||
|
||||
if is_conflict:
|
||||
output += "│\n"
|
||||
|
||||
if is_conflict:
|
||||
conflict_until_str = await manager.repo.get_setting("conflict_until")
|
||||
conflict_until = datetime.fromtimestamp(float(conflict_until_str))
|
||||
time_left_seconds = (conflict_until - datetime.now()).total_seconds()
|
||||
time_left_minutes = int(time_left_seconds / 60)
|
||||
|
||||
conflict_words_count = len(data.get('conflict_substring', set()))
|
||||
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
|
||||
total_conflict = conflict_words_count + conflict_lemmas_count
|
||||
|
||||
output += f"⚔️ <b>Режим антиконфликта</b>\n"
|
||||
output += f"├─ ⏱ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||||
output += f"├─ 🕐 До: {format_datetime(conflict_until)}\n"
|
||||
output += f"└─ 📊 Правил: <code>{total_conflict}</code>\n"
|
||||
|
||||
output += "\n"
|
||||
|
||||
# Статистика правил
|
||||
total_rules = (
|
||||
len(data.get('substring', set())) +
|
||||
len(data.get('lemma', set())) +
|
||||
len(data.get('part', set())) +
|
||||
len(data.get('temp_substring', set())) +
|
||||
len(data.get('temp_lemma', set())) +
|
||||
len(data.get('conflict_substring', set())) +
|
||||
len(data.get('conflict_lemma', set()))
|
||||
)
|
||||
|
||||
output += f"📋 <b>Правила модерации:</b>\n"
|
||||
output += f"├─ Всего правил: <code>{total_rules}</code>\n"
|
||||
output += f"├─ Постоянные: <code>{len(data.get('substring', set())) + len(data.get('lemma', set())) + len(data.get('part', set()))}</code>\n"
|
||||
output += f"├─ Временные: <code>{len(data.get('temp_substring', set())) + len(data.get('temp_lemma', set()))}</code>\n"
|
||||
output += f"├─ Конфликтные: <code>{len(data.get('conflict_substring', set())) + len(data.get('conflict_lemma', set()))}</code>\n"
|
||||
output += f"└─ Исключения: <code>{len(data.get('whitelist', set()))}</code>\n\n"
|
||||
|
||||
# Топ-5 спамеров
|
||||
if top_spammers:
|
||||
output += "🏆 <b>Топ-5 спамеров:</b>\n"
|
||||
max_count = top_spammers[0][1] if top_spammers else 1
|
||||
|
||||
for idx, (user_id, count) in enumerate(top_spammers, 1):
|
||||
bar = create_text_bar(count, max_count, length=8)
|
||||
output += f"{idx}. <code>{user_id}</code> — {count} [{bar}]\n"
|
||||
|
||||
output += "\n"
|
||||
else:
|
||||
output += "🏆 <b>Топ-5 спамеров:</b>\n"
|
||||
output += "└─ <i>Нет данных</i>\n\n"
|
||||
|
||||
# Администраторы
|
||||
admins_count = len(settings.OWNER_ID) + len(data.get('admins', set()))
|
||||
output += f"👥 <b>Администраторов:</b> <code>{admins_count}</code>\n\n"
|
||||
|
||||
# Подсказка
|
||||
output += "💡 <i>Используйте кнопки для детальной информации</i>"
|
||||
|
||||
# Клавиатура
|
||||
keyboard = get_stats_keyboard()
|
||||
|
||||
# Отправка
|
||||
if is_callback:
|
||||
await message.edit_text(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
await update.answer("✅ Статистика обновлена")
|
||||
else:
|
||||
await message.answer(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статистики: {e}", log_type="STATS")
|
||||
|
||||
error_text = "❌ <b>Ошибка загрузки статистики</b>\n\nПопробуйте позже"
|
||||
|
||||
if is_callback:
|
||||
await update.answer("❌ Ошибка загрузки", show_alert=True)
|
||||
else:
|
||||
await message.answer(error_text, parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= ДЕТАЛЬНАЯ СТАТИСТИКА =================
|
||||
|
||||
@router.callback_query(F.data == "stats:details")
|
||||
@log_action(action_name="VIEW_DETAILED_STATS")
|
||||
async def stats_details_callback(callback: CallbackQuery) -> None:
|
||||
"""Показывает детальную статистику"""
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
stats = await manager.get_stats()
|
||||
data = await manager.get_all_words_list()
|
||||
|
||||
output = "📊 <b>ДЕТАЛЬНАЯ СТАТИСТИКА</b>\n\n"
|
||||
|
||||
# Подробная статистика удалений
|
||||
total_deletions = stats.get('total_deletions', 0)
|
||||
output += f"🗑 <b>Удаления сообщений:</b>\n"
|
||||
output += f"├─ Всего: <code>{format_number(total_deletions)}</code>\n"
|
||||
output += "\n"
|
||||
|
||||
# Активные режимы (детально)
|
||||
is_silence = await manager.is_silence_active()
|
||||
is_conflict = await manager.is_conflict_active()
|
||||
|
||||
if is_silence or is_conflict:
|
||||
output += "🔴 <b>Активные режимы:</b>\n\n"
|
||||
|
||||
if is_silence:
|
||||
silence_until_str = await manager.repo.get_setting("silence_until")
|
||||
silence_until = datetime.fromtimestamp(float(silence_until_str))
|
||||
time_left_seconds = (silence_until - datetime.now()).total_seconds()
|
||||
time_left_minutes = int(time_left_seconds / 60)
|
||||
|
||||
output += f"🔇 <b>Режим тишины:</b>\n"
|
||||
output += f"├─ Статус: ✅ Активен\n"
|
||||
output += f"├─ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||||
output += f"├─ Окончание: {format_datetime(silence_until)}\n"
|
||||
output += f"└─ Эффект: Удаляются ВСЕ сообщения\n\n"
|
||||
|
||||
if is_conflict:
|
||||
conflict_until_str = await manager.repo.get_setting("conflict_until")
|
||||
conflict_until = datetime.fromtimestamp(float(conflict_until_str))
|
||||
time_left_seconds = (conflict_until - datetime.now()).total_seconds()
|
||||
time_left_minutes = int(time_left_seconds / 60)
|
||||
|
||||
conflict_words_count = len(data.get('conflict_substring', set()))
|
||||
conflict_lemmas_count = len(data.get('conflict_lemma', set()))
|
||||
|
||||
output += f"⚔️ <b>Режим антиконфликта:</b>\n"
|
||||
output += f"├─ Статус: ✅ Активен\n"
|
||||
output += f"├─ Осталось: <code>{format_time_remaining(time_left_minutes)}</code>\n"
|
||||
output += f"├─ Окончание: {format_datetime(conflict_until)}\n"
|
||||
output += f"├─ Слов: <code>{conflict_words_count}</code>\n"
|
||||
output += f"├─ Лемм: <code>{conflict_lemmas_count}</code>\n"
|
||||
output += f"└─ Эффект: Обычные банворды отключены\n\n"
|
||||
|
||||
# Детальная статистика правил
|
||||
output += f"📋 <b>Правила модерации:</b>\n\n"
|
||||
|
||||
output += f"🔴 <b>Постоянные:</b>\n"
|
||||
output += f"├─ Подстроки: <code>{len(data.get('substring', set()))}</code>\n"
|
||||
output += f"├─ Леммы: <code>{len(data.get('lemma', set()))}</code>\n"
|
||||
output += f"└─ Части: <code>{len(data.get('part', set()))}</code>\n\n"
|
||||
|
||||
output += f"⏱ <b>Временные:</b>\n"
|
||||
output += f"├─ Подстроки: <code>{len(data.get('temp_substring', set()))}</code>\n"
|
||||
output += f"└─ Леммы: <code>{len(data.get('temp_lemma', set()))}</code>\n\n"
|
||||
|
||||
output += f"⚔️ <b>Конфликтные:</b>\n"
|
||||
output += f"├─ Слова: <code>{len(data.get('conflict_substring', set()))}</code>\n"
|
||||
output += f"└─ Леммы: <code>{len(data.get('conflict_lemma', set()))}</code>\n\n"
|
||||
|
||||
output += f"✅ <b>Исключения:</b> <code>{len(data.get('whitelist', set()))}</code>\n\n"
|
||||
|
||||
# Информация о кэше
|
||||
cache_info = stats.get('cache_active', False)
|
||||
cache_updated = stats.get('cache_updated_at', None)
|
||||
|
||||
output += f"💾 <b>Кэш:</b>\n"
|
||||
output += f"├─ Статус: {'✅ Активен' if cache_info else '❌ Неактивен'}\n"
|
||||
|
||||
if cache_updated and isinstance(cache_updated, str):
|
||||
try:
|
||||
updated_dt = datetime.fromisoformat(cache_updated)
|
||||
output += f"└─ Обновлён: {format_datetime(updated_dt)}\n"
|
||||
except (ValueError, TypeError):
|
||||
output += f"└─ Обновлён: недавно\n"
|
||||
else:
|
||||
output += f"└─ Не обновлялся\n"
|
||||
|
||||
# Кнопка возврата
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="◀️ Назад", callback_data="stats:refresh")
|
||||
|
||||
await callback.message.edit_text(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения детальной статистики: {e}", log_type="STATS")
|
||||
await callback.answer("❌ Ошибка загрузки", show_alert=True)
|
||||
|
||||
|
||||
# ================= ТОП СПАМЕРОВ =================
|
||||
|
||||
@router.callback_query(F.data == "stats:top_spammers")
|
||||
@log_action(action_name="VIEW_TOP_SPAMMERS")
|
||||
async def stats_top_spammers_callback(callback: CallbackQuery) -> None:
|
||||
"""Показывает топ-10 спамеров"""
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
top_spammers = await manager.get_top_spammers(limit=10)
|
||||
|
||||
output = "🏆 <b>ТОП-10 СПАМЕРОВ</b>\n\n"
|
||||
|
||||
if top_spammers:
|
||||
max_count = top_spammers[0][1] if top_spammers else 1
|
||||
|
||||
for idx, (user_id, count) in enumerate(top_spammers, 1):
|
||||
bar = create_text_bar(count, max_count, length=10)
|
||||
|
||||
# Эмодзи для топ-3
|
||||
if idx == 1:
|
||||
medal = "🥇"
|
||||
elif idx == 2:
|
||||
medal = "🥈"
|
||||
elif idx == 3:
|
||||
medal = "🥉"
|
||||
else:
|
||||
medal = f"{idx}."
|
||||
|
||||
output += f"{medal} <code>{user_id}</code>\n"
|
||||
output += f" └─ {format_number(count)} удалений [{bar}]\n\n"
|
||||
|
||||
# Общая статистика
|
||||
total_spammers = len(top_spammers)
|
||||
total_deletions = sum(count for _, count in top_spammers)
|
||||
|
||||
output += f"📊 <b>Статистика:</b>\n"
|
||||
output += f"├─ Всего пользователей: <code>{total_spammers}</code>\n"
|
||||
output += f"└─ Всего удалений: <code>{format_number(total_deletions)}</code>\n\n"
|
||||
|
||||
output += "💡 <i>ID можно использовать для проверки пользователя</i>"
|
||||
else:
|
||||
output += "└─ <i>Нет данных об удалениях</i>\n\n"
|
||||
output += "💡 <i>Когда бот начнёт удалять сообщения, здесь появится статистика</i>"
|
||||
|
||||
# Кнопка возврата
|
||||
ikb = InlineKeyboardBuilder()
|
||||
ikb.button(text="◀️ Назад", callback_data="stats:refresh")
|
||||
|
||||
await callback.message.edit_text(
|
||||
text=output,
|
||||
parse_mode="HTML",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения топ спамеров: {e}", log_type="STATS")
|
||||
await callback.answer("❌ Ошибка загрузки", show_alert=True)
|
||||
|
||||
|
||||
# ================= ТОП СЛОВ =================
|
||||
|
||||
@router.callback_query(F.data == "stats_top_words")
|
||||
async def stats_top_words_callback(callback: CallbackQuery) -> None:
|
||||
"""Показывает топ-10 самых частых срабатываний"""
|
||||
await callback.answer()
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
# Получаем топ слов
|
||||
top_words = await manager.get_top_words(limit=10)
|
||||
|
||||
if not top_words:
|
||||
text = (
|
||||
"🔤 <b>ТОП-10 СРАБАТЫВАНИЙ ПО СЛОВАМ</b>\n\n"
|
||||
"📭 <i>Статистика пока пуста</i>\n\n"
|
||||
"Срабатывания появятся после удаления\n"
|
||||
"первых спам-сообщений."
|
||||
)
|
||||
else:
|
||||
text = "🔤 <b>ТОП-10 СРАБАТЫВАНИЙ ПО СЛОВАМ</b>\n\n"
|
||||
|
||||
# Эмодзи для типов
|
||||
type_emoji = {
|
||||
"substring": "🔤",
|
||||
"lemma": "📖",
|
||||
"part": "🧩",
|
||||
"silence": "🔇",
|
||||
"conflict_substring": "⚔️",
|
||||
"conflict_lemma": "⚔️"
|
||||
}
|
||||
|
||||
for i, word_data in enumerate(top_words, 1):
|
||||
word = word_data['word']
|
||||
count = word_data['count']
|
||||
word_type = word_data['type']
|
||||
emoji = type_emoji.get(word_type, "❓")
|
||||
|
||||
# Медали для топ-3
|
||||
medal = ""
|
||||
if i == 1:
|
||||
medal = "🥇 "
|
||||
elif i == 2:
|
||||
medal = "🥈 "
|
||||
elif i == 3:
|
||||
medal = "🥉 "
|
||||
|
||||
text += f"{medal}<b>{i}.</b> {emoji} <code>{word}</code> — {count} раз\n"
|
||||
|
||||
# Общая статистика
|
||||
total = await manager.get_total_spam_count()
|
||||
text += f"\n📊 <b>Всего удалено:</b> {total} сообщений"
|
||||
|
||||
# Кнопка назад
|
||||
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||
[InlineKeyboardButton(text="◀️ Назад", callback_data="show_stats")]
|
||||
])
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text=text,
|
||||
reply_markup=keyboard,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка показа топ-слов: {e}", log_type="ERROR")
|
||||
await callback.answer("❌ Ошибка загрузки статистики", show_alert=True)
|
||||
|
||||
|
||||
# ================= СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("userstats", ["userstats"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="VIEW_USER_STATS", log_args=True)
|
||||
async def user_stats_cmd(message: Message) -> None:
|
||||
"""
|
||||
Показывает статистику конкретного пользователя.
|
||||
|
||||
Использование: /userstats <ID>
|
||||
Пример: /userstats 123456789
|
||||
"""
|
||||
parts = message.text.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2:
|
||||
await message.answer(
|
||||
"❌ Использование: <code>/userstats [ID]</code>\n\n"
|
||||
"Пример: <code>/userstats 123456789</code>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = int(parts[1].strip())
|
||||
except ValueError:
|
||||
await message.answer("❌ ID должен быть числом", parse_mode="HTML")
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Получаем статистику пользователя
|
||||
user_spam_count = await manager.get_user_spam_count(user_id)
|
||||
user_spam_stats = await manager.get_spam_stats(limit=10, user_id=user_id)
|
||||
|
||||
output = f"👤 <b>СТАТИСТИКА ПОЛЬЗОВАТЕЛЯ</b>\n\n"
|
||||
output += f"🆔 ID: <code>{user_id}</code>\n\n"
|
||||
|
||||
if user_spam_count > 0:
|
||||
output += f"🗑 <b>Удалено сообщений:</b> <code>{format_number(user_spam_count)}</code>\n\n"
|
||||
|
||||
if user_spam_stats:
|
||||
output += f"📝 <b>Последние удаления:</b>\n"
|
||||
|
||||
for stat in user_spam_stats[:5]:
|
||||
deleted_at = stat.deleted_at
|
||||
matched_word = stat.matched_word or "неизвестно"
|
||||
match_type = stat.match_type or "unknown"
|
||||
|
||||
output += f"├─ {format_datetime(deleted_at)}\n"
|
||||
output += f"│ └─ Слово: <code>{matched_word}</code> ({match_type})\n"
|
||||
|
||||
output += "\n"
|
||||
else:
|
||||
output += "✅ <i>Нет нарушений</i>\n\n"
|
||||
output += "Этот пользователь не нарушал правила чата"
|
||||
|
||||
await message.answer(output, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения статистики пользователя: {e}", log_type="STATS")
|
||||
await message.answer("❌ <b>Ошибка загрузки статистики</b>", parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= СБРОС СТАТИСТИКИ =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("resetstats", ["resetstats"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="RESET_STATS")
|
||||
async def reset_stats_cmd(message: Message) -> None:
|
||||
"""
|
||||
Сбрасывает всю статистику удалений.
|
||||
|
||||
⚠️ ВНИМАНИЕ: Это действие необратимо!
|
||||
|
||||
Использование: /resetstats confirm
|
||||
"""
|
||||
parts = message.text.split(maxsplit=1)
|
||||
|
||||
if len(parts) < 2 or parts[1].lower() != "confirm":
|
||||
await message.answer(
|
||||
"⚠️ <b>ВНИМАНИЕ!</b>\n\n"
|
||||
"Эта команда удалит ВСЮ статистику удалений:\n"
|
||||
"• Счётчики удалений пользователей\n"
|
||||
"• Историю удалённых сообщений\n"
|
||||
"• Топ спамеров\n\n"
|
||||
"Правила модерации НЕ будут удалены.\n\n"
|
||||
"Для подтверждения используйте:\n"
|
||||
"<code>/resetstats confirm</code>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
# Сбрасываем статистику
|
||||
deleted_count = await manager.reset_spam_stats()
|
||||
|
||||
if deleted_count > 0:
|
||||
await message.answer(
|
||||
f"✅ <b>Статистика сброшена</b>\n\n"
|
||||
f"Удалено записей: {deleted_count}\n\n"
|
||||
f"Новые данные начнут собираться\n"
|
||||
f"с этого момента.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
logger.warning(
|
||||
f"Статистика сброшена пользователем {message.from_user.id}: "
|
||||
f"удалено {deleted_count} записей",
|
||||
log_type="STATS"
|
||||
)
|
||||
else:
|
||||
await message.answer(
|
||||
"ℹ️ <b>Статистика уже пуста</b>",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сброса статистики: {e}", log_type="STATS")
|
||||
await message.answer("❌ <b>Ошибка сброса статистики</b>", parse_mode="HTML")
|
||||
|
||||
546
bot/handlers/commands/users/word.py
Normal file
546
bot/handlers/commands/users/word.py
Normal file
@@ -0,0 +1,546 @@
|
||||
"""
|
||||
Обработчики команд добавления и удаления банвордов
|
||||
"""
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot.filters.admin import IsAdmin
|
||||
from configs import settings, COMMANDS
|
||||
from database import get_manager
|
||||
from database.models import BanWordType
|
||||
from middleware.loggers import logger
|
||||
from bot.utils.decorators import log_action
|
||||
|
||||
__all__ = ("router",)
|
||||
|
||||
router: Router = Router(name="manage_words_router")
|
||||
|
||||
|
||||
# ================= ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ =================
|
||||
|
||||
def parse_args(text: str, command: str, min_args: int = 1, max_args: int = 2) -> tuple[bool, str | list]:
|
||||
"""
|
||||
Парсит аргументы команды.
|
||||
|
||||
Args:
|
||||
text: Полный текст сообщения
|
||||
command: Название команды
|
||||
min_args: Минимальное количество аргументов
|
||||
max_args: Максимальное количество аргументов
|
||||
|
||||
Returns:
|
||||
(success, result): result это либо список аргументов, либо текст ошибки
|
||||
"""
|
||||
# Убираем команду из текста
|
||||
parts = text.split(maxsplit=max_args)
|
||||
|
||||
if len(parts) < min_args + 1:
|
||||
return False, f"❌ Использование: <code>/{command} {'<слово>' if min_args == 1 else '<слово> <минуты>'}</code>"
|
||||
|
||||
args = parts[1:]
|
||||
|
||||
# Валидация длины слова
|
||||
if args and len(args[0]) < 2:
|
||||
return False, "❌ Слово должно содержать минимум 2 символа"
|
||||
|
||||
if args and len(args[0]) > 100:
|
||||
return False, "❌ Слово слишком длинное (максимум 100 символов)"
|
||||
|
||||
return True, args
|
||||
|
||||
|
||||
def format_success_message(action: str, word: str, word_type: str, extra: str = "") -> str:
|
||||
"""Форматирует сообщение об успехе"""
|
||||
emoji_map = {
|
||||
'добавлена': '✅',
|
||||
'добавлен': '✅',
|
||||
'добавлено': '✅',
|
||||
'удалена': '🗑',
|
||||
'удален': '🗑',
|
||||
'удалено': '🗑'
|
||||
}
|
||||
|
||||
emoji = emoji_map.get(action, '✅')
|
||||
|
||||
message = f"{emoji} <b>{word_type.capitalize()}</b> <code>{word}</code> {action}"
|
||||
|
||||
if extra:
|
||||
message += f"\n{extra}"
|
||||
|
||||
return message
|
||||
|
||||
|
||||
# ================= КОМАНДЫ ДОБАВЛЕНИЯ =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addword", ["addword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="ADD_WORD", log_args=True)
|
||||
async def add_word_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет банворд-подстроку (постоянно).
|
||||
|
||||
Использование: /addword <слово>
|
||||
"""
|
||||
success, result = parse_args(message.text, "addword", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.SUBSTRING,
|
||||
added_by=message.from_user.id,
|
||||
reason=f"Добавлено через команду"
|
||||
)
|
||||
|
||||
if added:
|
||||
text = format_success_message(
|
||||
"добавлена",
|
||||
word,
|
||||
"подстрока",
|
||||
"🔍 Тип проверки: простой поиск в тексте"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Подстрока <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления банворда: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addlemma", ["addlemma"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="ADD_LEMMA", log_args=True)
|
||||
async def add_lemma_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет банворд-лемму (постоянно).
|
||||
|
||||
Использование: /addlemma <слово>
|
||||
"""
|
||||
success, result = parse_args(message.text, "addlemma", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.LEMMA,
|
||||
added_by=message.from_user.id,
|
||||
reason=f"Добавлено через команду"
|
||||
)
|
||||
|
||||
if added:
|
||||
text = format_success_message(
|
||||
"добавлена",
|
||||
word,
|
||||
"лемма",
|
||||
"🔤 Тип проверки: все формы слова (купить→куплю, купил, купишь...)"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Лемма <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления леммы: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addpart", ["addpart"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="ADD_PART", log_args=True)
|
||||
async def add_part_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет банворд-часть (постоянно).
|
||||
|
||||
Использование: /addpart <комбинация>
|
||||
"""
|
||||
success, result = parse_args(message.text, "addpart", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.PART,
|
||||
added_by=message.from_user.id,
|
||||
reason=f"Добавлено через команду"
|
||||
)
|
||||
|
||||
if added:
|
||||
text = format_success_message(
|
||||
"добавлена",
|
||||
word,
|
||||
"часть",
|
||||
"🧩 Тип проверки: поиск без пробелов (обходит \"к у п и т ь\")"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Часть <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления части: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addtempword", ["addtempword"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="ADD_TEMP_WORD", log_args=True)
|
||||
async def add_temp_word_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет временную банворд-подстроку.
|
||||
|
||||
Использование: /addtempword <слово> <минуты>
|
||||
"""
|
||||
success, result = parse_args(message.text, "addtempword", min_args=2, max_args=2)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
|
||||
# Валидация минут
|
||||
try:
|
||||
minutes = int(result[1])
|
||||
if minutes < 1 or minutes > 10080: # Максимум неделя
|
||||
await message.answer("❌ Время должно быть от 1 минуты до 10080 минут (7 дней)", parse_mode="HTML")
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_temp_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.SUBSTRING,
|
||||
minutes=minutes,
|
||||
added_by=message.from_user.id
|
||||
)
|
||||
|
||||
if added:
|
||||
# Форматируем время
|
||||
if minutes < 60:
|
||||
time_str = f"{minutes} мин"
|
||||
elif minutes < 1440:
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
time_str = f"{hours}ч {mins}м" if mins else f"{hours}ч"
|
||||
else:
|
||||
days = minutes // 1440
|
||||
hours = (minutes % 1440) // 60
|
||||
time_str = f"{days}д {hours}ч" if hours else f"{days}д"
|
||||
|
||||
text = format_success_message(
|
||||
"добавлена",
|
||||
word,
|
||||
"временная подстрока",
|
||||
f"⏱ Автоматически удалится через {time_str}"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Временная подстрока <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления временного банворда: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addtemplemma", ["addtemplemma"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="ADD_TEMP_LEMMA", log_args=True)
|
||||
async def add_temp_lemma_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет временную банворд-лемму.
|
||||
|
||||
Использование: /addtemplemma <слово> <минуты>
|
||||
"""
|
||||
success, result = parse_args(message.text, "addtemplemma", min_args=2, max_args=2)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
|
||||
try:
|
||||
minutes = int(result[1])
|
||||
if minutes < 1 or minutes > 10080:
|
||||
await message.answer("❌ Время должно быть от 1 минуты до 10080 минут (7 дней)", parse_mode="HTML")
|
||||
return
|
||||
except ValueError:
|
||||
await message.answer("❌ Неверный формат времени. Укажите число минут", parse_mode="HTML")
|
||||
return
|
||||
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_temp_banword(
|
||||
word=word,
|
||||
word_type=BanWordType.LEMMA,
|
||||
minutes=minutes,
|
||||
added_by=message.from_user.id
|
||||
)
|
||||
|
||||
if added:
|
||||
if minutes < 60:
|
||||
time_str = f"{minutes} мин"
|
||||
elif minutes < 1440:
|
||||
hours = minutes // 60
|
||||
mins = minutes % 60
|
||||
time_str = f"{hours}ч {mins}м" if mins else f"{hours}ч"
|
||||
else:
|
||||
days = minutes // 1440
|
||||
hours = (minutes % 1440) // 60
|
||||
time_str = f"{days}д {hours}ч" if hours else f"{days}д"
|
||||
|
||||
text = format_success_message(
|
||||
"добавлена",
|
||||
word,
|
||||
"временная лемма",
|
||||
f"⏱ Автоматически удалится через {time_str}"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Временная лемма <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления временной леммы: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("addexcept", ["addexcept"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="ADD_EXCEPTION", log_args=True)
|
||||
async def add_exception_cmd(message: Message) -> None:
|
||||
"""
|
||||
Добавляет исключение в whitelist.
|
||||
|
||||
Использование: /addexcept <текст>
|
||||
"""
|
||||
success, result = parse_args(message.text, "addexcept", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
added = await manager.add_whitelist(
|
||||
word=word,
|
||||
added_by=message.from_user.id,
|
||||
reason="Добавлено через команду"
|
||||
)
|
||||
|
||||
if added:
|
||||
text = format_success_message(
|
||||
"добавлено",
|
||||
word,
|
||||
"исключение",
|
||||
"✅ Сообщения с этим текстом не будут проверяться"
|
||||
)
|
||||
else:
|
||||
text = f"⚠️ Исключение <code>{word}</code> уже существует"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка добавления исключения: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка добавления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
# ================= КОМАНДЫ УДАЛЕНИЯ =================
|
||||
|
||||
@router.message(Command(*COMMANDS.get("remword", ["remword"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="REMOVE_WORD", log_args=True)
|
||||
async def remove_word_cmd(message: Message) -> None:
|
||||
"""
|
||||
Удаляет банворд-подстроку.
|
||||
|
||||
Использование: /remword <слово>
|
||||
"""
|
||||
success, result = parse_args(message.text, "remword", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_banword(word=word, word_type=BanWordType.SUBSTRING)
|
||||
|
||||
if removed:
|
||||
text = format_success_message("удалена", word, "подстрока")
|
||||
else:
|
||||
text = f"⚠️ Подстрока <code>{word}</code> не найдена"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления банворда: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("remlemma", ["remlemma"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="REMOVE_LEMMA", log_args=True)
|
||||
async def remove_lemma_cmd(message: Message) -> None:
|
||||
"""Удаляет банворд-лемму"""
|
||||
success, result = parse_args(message.text, "remlemma", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_banword(word=word, word_type=BanWordType.LEMMA)
|
||||
|
||||
if removed:
|
||||
text = format_success_message("удалена", word, "лемма")
|
||||
else:
|
||||
text = f"⚠️ Лемма <code>{word}</code> не найдена"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления леммы: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("rempart", ["rempart"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="REMOVE_PART", log_args=True)
|
||||
async def remove_part_cmd(message: Message) -> None:
|
||||
"""Удаляет банворд-часть"""
|
||||
success, result = parse_args(message.text, "rempart", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_banword(word=word, word_type=BanWordType.PART)
|
||||
|
||||
if removed:
|
||||
text = format_success_message("удалена", word, "часть")
|
||||
else:
|
||||
text = f"⚠️ Часть <code>{word}</code> не найдена"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления части: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("remtempword", ["remtempword"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="REMOVE_TEMP_WORD", log_args=True)
|
||||
async def remove_temp_word_cmd(message: Message) -> None:
|
||||
"""Удаляет временную подстроку"""
|
||||
success, result = parse_args(message.text, "remtempword", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_temp_banword(word=word, word_type=BanWordType.SUBSTRING)
|
||||
|
||||
if removed:
|
||||
text = format_success_message("удалена", word, "временная подстрока")
|
||||
else:
|
||||
text = f"⚠️ Временная подстрока <code>{word}</code> не найдена"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления временного банворда: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("remtemplemma", ["remtemplemma"]), prefix=settings.PREFIX, ignore_case=True),
|
||||
IsAdmin())
|
||||
@log_action(action_name="REMOVE_TEMP_LEMMA", log_args=True)
|
||||
async def remove_temp_lemma_cmd(message: Message) -> None:
|
||||
"""Удаляет временную лемму"""
|
||||
success, result = parse_args(message.text, "remtemplemma", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_temp_banword(word=word, word_type=BanWordType.LEMMA)
|
||||
|
||||
if removed:
|
||||
text = format_success_message("удалена", word, "временная лемма")
|
||||
else:
|
||||
text = f"⚠️ Временная лемма <code>{word}</code> не найдена"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления временной леммы: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
|
||||
|
||||
@router.message(Command(*COMMANDS.get("remexcept", ["remexcept"]), prefix=settings.PREFIX, ignore_case=True), IsAdmin())
|
||||
@log_action(action_name="REMOVE_EXCEPTION", log_args=True)
|
||||
async def remove_exception_cmd(message: Message) -> None:
|
||||
"""Удаляет исключение из whitelist"""
|
||||
success, result = parse_args(message.text, "remexcept", min_args=1, max_args=1)
|
||||
|
||||
if not success:
|
||||
await message.answer(result, parse_mode="HTML")
|
||||
return
|
||||
|
||||
word = result[0].lower().strip()
|
||||
manager = get_manager()
|
||||
|
||||
try:
|
||||
removed = await manager.remove_whitelist(word=word)
|
||||
|
||||
if removed:
|
||||
text = format_success_message("удалено", word, "исключение")
|
||||
else:
|
||||
text = f"⚠️ Исключение <code>{word}</code> не найдено"
|
||||
|
||||
await message.answer(text, parse_mode="HTML")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка удаления исключения: {e}", log_type="CMD")
|
||||
await message.answer("❌ <b>Ошибка удаления</b>\n\nПопробуйте позже", parse_mode="HTML")
|
||||
Reference in New Issue
Block a user