First commit
This commit is contained in:
21
bot/handlers/commands/__init__.py
Normal file
21
bot/handlers/commands/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from aiogram import Router
|
||||
|
||||
from .admins import router as admin_cmd_router
|
||||
from .special import router as special_cmd_router
|
||||
from .users import router as users_cmd_router
|
||||
from .users.cancel_cmd import router as cancel_cmd_router
|
||||
from .settings import router as settings_cmd_router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
cancel_cmd_router,
|
||||
settings_cmd_router,
|
||||
admin_cmd_router,
|
||||
users_cmd_router,
|
||||
special_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,
|
||||
|
||||
)
|
||||
80
bot/handlers/commands/admins/all_cmd.py
Normal file
80
bot/handlers/commands/admins/all_cmd.py
Normal file
@@ -0,0 +1,80 @@
|
||||
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),
|
||||
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(message=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(message=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(message=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(message=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("❌ Не удалось получить список забаненных пользователей")
|
||||
278
bot/handlers/commands/admins/kick_cmd.py
Normal file
278
bot/handlers/commands/admins/kick_cmd.py
Normal file
@@ -0,0 +1,278 @@
|
||||
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(message=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}) кикнут из чата!",
|
||||
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\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(message=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(message=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)
|
||||
0
bot/handlers/commands/admins/mute_cmd.py
Normal file
0
bot/handlers/commands/admins/mute_cmd.py
Normal file
55
bot/handlers/commands/admins/pin_cmd.py
Normal file
55
bot/handlers/commands/admins/pin_cmd.py
Normal file
@@ -0,0 +1,55 @@
|
||||
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 для закрепления последнего сообщения или ответа.
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Если есть reply → закрепляем его, иначе закрепляем саму команду
|
||||
target = message.reply_to_message or message
|
||||
|
||||
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(message=message, text="✅ Сообщение успешно закреплено")
|
||||
|
||||
|
||||
@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(message=callback.message, state=state)
|
||||
|
||||
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("✅ Сообщение закреплено")
|
||||
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=RpValue.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(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||
0
bot/handlers/commands/admins/varn_cmd.py
Normal file
0
bot/handlers/commands/admins/varn_cmd.py
Normal file
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,
|
||||
)
|
||||
167
bot/handlers/commands/settings/set_description_cmd.py
Normal file
167
bot/handlers/commands/settings/set_description_cmd.py
Normal file
@@ -0,0 +1,167 @@
|
||||
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(
|
||||
message=message,
|
||||
text=_("❌ Короткое описание бота должно быть не более 120 символов. Текущая длина: {length}").format(
|
||||
length=len(description)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Установка нового короткого описания
|
||||
await bot.set_my_short_description(short_description=description)
|
||||
|
||||
# Сохраняем текущее значение в BotInfo
|
||||
BotInfo.short_description = description
|
||||
|
||||
# Сбрасываем состояние FSM
|
||||
await state.clear()
|
||||
|
||||
# Отправляем сообщение об успехе
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("✅ Короткое описание бота успешно изменено на: <b>{description}</b>").format(
|
||||
description=description
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
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(
|
||||
message=message,
|
||||
text=_("⚠️ Слишком частая смена короткого описания!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
logger.error(f"Ошибка Telegram API при изменении короткого описания: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Ошибка Telegram API при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при изменении короткого описания: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Непредвиденная ошибка при изменении короткого описания: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@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(
|
||||
message=message,
|
||||
text=_("❌ Короткое описание не должно превышать 120 символов. Текущая длина: {length}").format(
|
||||
length=len(description)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
await handle_set_bot_description(description, message, state, bot)
|
||||
return
|
||||
|
||||
# Вариант 2: без аргумента → включаем FSM
|
||||
await status_clear(message=message, state=state)
|
||||
text: str = _(
|
||||
"📝 <b>Смена короткого описания бота</b>\n\n"
|
||||
"Текущее короткое описание: <i>{current}</i>\n\n"
|
||||
"Введите новое короткое описание (максимум 120 символов):"
|
||||
).format(current=current_description)
|
||||
|
||||
await msg(message=message, text=text, markup=settings_keyboard())
|
||||
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)
|
||||
151
bot/handlers/commands/settings/set_name_cmd.py
Normal file
151
bot/handlers/commands/settings/set_name_cmd.py
Normal file
@@ -0,0 +1,151 @@
|
||||
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(
|
||||
message=message,
|
||||
text=_("❌ Имя бота должно быть не более 64 символов. Текущая длина: {length}").format(
|
||||
length=len(new_name)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await bot.set_my_name(new_name)
|
||||
BotInfo.first_name = new_name
|
||||
await state.clear()
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("✅ Имя бота успешно изменено на: <b>{new_name}</b>").format(new_name=new_name),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
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(
|
||||
message=message,
|
||||
text=_("⚠️ Слишком частая смена имени!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
logger.error(f"Ошибка Telegram API при изменении имени: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Ошибка Telegram API: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Непредвиденная ошибка при изменении имени: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Непредвиденная ошибка: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@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(
|
||||
message=message,
|
||||
text=_("❌ Имя не должно превышать 64 символа. Текущая длина: {length}").format(
|
||||
length=len(new_name)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
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(message=message, text=text, markup=settings_keyboard())
|
||||
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)
|
||||
168
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
168
bot/handlers/commands/settings/set_widget_cmd.py
Normal file
@@ -0,0 +1,168 @@
|
||||
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(
|
||||
message=message,
|
||||
text=_("❌ Виджет бота должен быть не более 512 символов. Текущая длина: {length}").format(
|
||||
length=len(new_widget)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
# Устанавливаем описание через Telegram API
|
||||
await bot.set_my_description(description=new_widget)
|
||||
|
||||
# Сохраняем в BotInfo для локального использования
|
||||
BotInfo.widget = new_widget
|
||||
|
||||
# Очищаем состояние FSM
|
||||
await state.clear()
|
||||
|
||||
# Отправляем уведомление пользователю
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("✅ Виджет бота успешно изменён на: <b>{new_widget}</b>").format(
|
||||
new_widget=new_widget
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
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(
|
||||
message=message,
|
||||
text=_("⚠️ Слишком частая смена виджета!\nПопробуйте снова через: <b>{retry_text}</b>").format(
|
||||
retry_text=retry_text
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except TelegramAPIError as e:
|
||||
# Ошибка Telegram API
|
||||
logger.error(f"Ошибка Telegram API при изменении виджета: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Ошибка Telegram API при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Непредвиденная ошибка
|
||||
logger.error(f"Непредвиденная ошибка при изменении виджета: {e}")
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Непредвиденная ошибка при изменении виджета: <pre>{error}</pre>").format(error=str(e)),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
|
||||
|
||||
@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.widget
|
||||
|
||||
# Вариант 1: пользователь ввёл аргумент сразу (/set_widget TEXT)
|
||||
if command and command.args:
|
||||
new_widget: str = command.args.strip()
|
||||
if len(new_widget) > 512:
|
||||
await msg(
|
||||
message=message,
|
||||
text=_("❌ Виджет не должен превышать 512 символов. Текущая длина: {length}").format(
|
||||
length=len(new_widget)
|
||||
),
|
||||
markup=settings_keyboard(),
|
||||
)
|
||||
return
|
||||
|
||||
await handle_set_widget(new_widget, message, state, bot)
|
||||
return
|
||||
|
||||
# Вариант 2: Callback query или пустая команда → запускаем FSM
|
||||
await status_clear(message=message, state=state)
|
||||
text: str = _(
|
||||
"📝 <b>Смена виджета бота</b>\n\n"
|
||||
"Текущий виджет: <i>{current}</i>\n\n"
|
||||
"Пожалуйста, введите новый виджет для бота (максимум 512 символов):"
|
||||
).format(current=current_widget)
|
||||
|
||||
await msg(message=message, text=text, markup=settings_keyboard())
|
||||
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(message=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(message=message, text=text, markup=ikb)
|
||||
9
bot/handlers/commands/special/__init__.py
Normal file
9
bot/handlers/commands/special/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from aiogram import Router
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
# Подключение роутеров
|
||||
# router.include_routers(
|
||||
# )
|
||||
22
bot/handlers/commands/users/__init__.py
Normal file
22
bot/handlers/commands/users/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from aiogram import Router
|
||||
|
||||
#from .active import router as active_cmd_router
|
||||
from .start_cmd import router as start_cmd_router
|
||||
#from .union_cmd import router as union_cmd_router
|
||||
from .new_cmd import router as new_cmd_router
|
||||
#from .create_cmd import router as create_cmd_router
|
||||
#from .anon import router as anon_router
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name=__name__)
|
||||
|
||||
|
||||
# Подключение роутеров
|
||||
router.include_routers(
|
||||
start_cmd_router,
|
||||
#active_cmd_router,
|
||||
#union_cmd_router,
|
||||
new_cmd_router,
|
||||
#create_cmd_router,
|
||||
#anon_router,
|
||||
)
|
||||
42
bot/handlers/commands/users/active.py
Normal file
42
bot/handlers/commands/users/active.py
Normal file
@@ -0,0 +1,42 @@
|
||||
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
|
||||
from bot.templates import msg_photo
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
from database import db
|
||||
|
||||
# Настройки экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
CMD: str = "active".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 active_cmd(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /active"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
# Получить статистику сообщений пользователя
|
||||
day, week, month, total = await db.get_message_stats(message.from_user.id)
|
||||
|
||||
print(f"За день: {day} сообщений")
|
||||
print(f"За неделю: {week} сообщений")
|
||||
print(f"За месяц: {month} сообщений")
|
||||
print(f"Всего: {total} сообщений")
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = f"""
|
||||
За день: {day} сообщений
|
||||
За неделю: {week} сообщений
|
||||
За месяц: {month} сообщений
|
||||
Всего: {total} сообщений
|
||||
"""
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, )
|
||||
117
bot/handlers/commands/users/anon.py
Normal file
117
bot/handlers/commands/users/anon.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from typing import Dict, Tuple
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from bot.utils import status_clear
|
||||
|
||||
# -------------------
|
||||
# Router
|
||||
# -------------------
|
||||
router: Router = Router(name="anon_router")
|
||||
|
||||
# -------------------
|
||||
# Конфигурация
|
||||
# -------------------
|
||||
# CHAT_ID в формате "-100000_29" -> chat_id + thread_id
|
||||
CHAT_ID: str = "-1003098225669_724"
|
||||
|
||||
def parse_chat_id(chat_id_str: str) -> Tuple[int, int]:
|
||||
chat_str, thread_str = chat_id_str.split("_")
|
||||
return int(chat_str), int(thread_str)
|
||||
|
||||
ADMIN_CHAT_ID, ADMIN_THREAD_ID = parse_chat_id(CHAT_ID)
|
||||
|
||||
# -------------------
|
||||
# FSM состояния
|
||||
# -------------------
|
||||
class AnonStates:
|
||||
USER_WAITING_TEXT = "user_waiting_text"
|
||||
ADMIN_WAITING_REPLY = "admin_waiting_reply"
|
||||
|
||||
# -------------------
|
||||
# Словари для отслеживания сообщений
|
||||
# -------------------
|
||||
# user_id -> message_id в админском топике
|
||||
user_to_admin_map: Dict[int, int] = {}
|
||||
# admin_message_id -> user_id
|
||||
admin_to_user_map: Dict[int, int] = {}
|
||||
|
||||
# -------------------
|
||||
# Команда /anon или callback
|
||||
# -------------------
|
||||
@router.callback_query(F.data.casefold() == "anon")
|
||||
@router.message(Command("anon"))
|
||||
async def anon_start(message: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Начало анонимного сообщения. Ждём текст пользователя."""
|
||||
await status_clear(message=message, state=state)
|
||||
await state.clear()
|
||||
await state.set_state(AnonStates.USER_WAITING_TEXT)
|
||||
|
||||
text = "Напишите сообщение, которое вы хотите отправить анонимно администраторам."
|
||||
if isinstance(message, Message):
|
||||
await message.reply(text)
|
||||
else:
|
||||
await message.message.answer(text)
|
||||
|
||||
# -------------------
|
||||
# Получение текста от пользователя
|
||||
# -------------------
|
||||
@router.message(F.text, F.state == AnonStates.USER_WAITING_TEXT)
|
||||
async def anon_send_text(message: Message, state: FSMContext) -> None:
|
||||
"""Пересылает текст пользователя в админский топик анонимно."""
|
||||
anon_text = message.text.strip()
|
||||
if not anon_text:
|
||||
await message.reply("Сообщение не может быть пустым. Попробуйте снова.")
|
||||
return
|
||||
|
||||
forwarded_text = f"Сообщение от [пользователя](tg://user?id={message.from_user.id}):\n{anon_text}"
|
||||
|
||||
keyboard = InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[InlineKeyboardButton(text="Ответить", callback_data=f"anon_reply:{message.from_user.id}")]
|
||||
]
|
||||
)
|
||||
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ADMIN_CHAT_ID,
|
||||
message_thread_id=ADMIN_THREAD_ID,
|
||||
text=forwarded_text,
|
||||
parse_mode="Markdown",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
|
||||
user_to_admin_map[message.from_user.id] = sent_msg.message_id
|
||||
admin_to_user_map[sent_msg.message_id] = message.from_user.id
|
||||
|
||||
await message.reply("Ваше сообщение отправлено анонимно администраторам.")
|
||||
await state.clear()
|
||||
|
||||
# -------------------
|
||||
# Кнопка "Ответить" админа
|
||||
# -------------------
|
||||
@router.callback_query(F.data.startswith("anon_reply:"))
|
||||
async def anon_admin_reply(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Начинаем сессию ответа админа пользователю."""
|
||||
user_id = int(callback.data.split(":")[1])
|
||||
await state.set_state(AnonStates.ADMIN_WAITING_REPLY)
|
||||
await state.update_data(reply_to_user=user_id)
|
||||
await callback.message.answer(f"Введите ответ для пользователя [id={user_id}]:")
|
||||
await callback.answer()
|
||||
|
||||
# -------------------
|
||||
# Текст ответа админа
|
||||
# -------------------
|
||||
@router.message(F.text, F.state == AnonStates.ADMIN_WAITING_REPLY)
|
||||
async def anon_send_admin_text(message: Message, state: FSMContext) -> None:
|
||||
"""Пересылает текст админа пользователю."""
|
||||
data = await state.get_data()
|
||||
reply_to_user = data.get("reply_to_user")
|
||||
|
||||
if reply_to_user:
|
||||
await message.bot.send_message(
|
||||
chat_id=reply_to_user,
|
||||
text=f"Ответ администратора:\n{message.text}"
|
||||
)
|
||||
await message.reply("Сообщение отправлено пользователю.")
|
||||
await state.clear()
|
||||
27
bot/handlers/commands/users/cancel_cmd.py
Normal file
27
bot/handlers/commands/users/cancel_cmd.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message
|
||||
|
||||
from bot import BotInfo
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS
|
||||
from middleware.loggers import logger
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "cancel".casefold()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
@router.message(F.text.casefold().in_(COMMANDS[CMD]))
|
||||
async def cancel_handler(message: Message, state: FSMContext, text: str = "❌ Отмена предыдущего действия!"):
|
||||
"""
|
||||
Позволяет пользователю отменить процесс смены описания
|
||||
"""
|
||||
await status_clear(message=message, state=state)
|
||||
|
||||
logger.info(text=text)
|
||||
|
||||
await message.answer(text)
|
||||
49
bot/handlers/commands/users/create_cmd.py
Normal file
49
bot/handlers/commands/users/create_cmd.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# bot/handlers/commands/create_cmd.py
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from bot.core import BotInfo
|
||||
from bot.states.anketa_states import StartForm
|
||||
from bot.templates import msg_photo
|
||||
from middleware import log
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
router: Router = Router(name="create_cmd_router")
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data == "create")
|
||||
@router.message(Command('create','скуфеу', 'анкета', prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type='Start', text="использовал(а) команду /create")
|
||||
async def create_cmd(message: Message|CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /create.
|
||||
"""
|
||||
# Сбросим все состояния (отменим создание поста, если оно было)
|
||||
await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Правила❗️", url='https://teletype.in/@velli_arsaan/XxUiHcB4Puj'))
|
||||
ikb.row(InlineKeyboardButton(text="Назад↪️", callback_data='start'))
|
||||
|
||||
# Создание базовых переменных сообщения
|
||||
caption: str = f"""
|
||||
Если вы хотели бы вступить в наш проект, то напоминаю, что вам сначала нужно ознакомиться с <b>инфо-каналом</b>! При продолжении диалога вы автоматически подтверждаете то, что прочитали все правила и в курсе, что мы ролевой проект, не флуд.
|
||||
<blockquote>Чтобы вступить к вам мы просим вас заполнить небольшую анкету:
|
||||
1. <i>Желаемая роль</i>;
|
||||
2. <i>Кого бы вы хотели в соролы?</i>;
|
||||
3. <i>Кодовая фраза из наших правил</i>;</blockquote>
|
||||
[‼️] Оно состоит всего из 4 слов, которые разбросаны в верном порядке по статьям о правилах.
|
||||
"""
|
||||
# Установим состояние ожидания анкеты
|
||||
await state.set_state(StartForm.waiting_for_application)
|
||||
|
||||
# Обработчик ответа на сообщение
|
||||
await msg_photo(message=message, text=caption, file='assets/help.png', markup=ikb)
|
||||
|
||||
368
bot/handlers/commands/users/new_cmd.py
Normal file
368
bot/handlers/commands/users/new_cmd.py
Normal file
@@ -0,0 +1,368 @@
|
||||
from typing import Dict
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command, StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import Message, InlineKeyboardButton, CallbackQuery
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from bot.core.bots import BotInfo
|
||||
from bot.utils import status_clear
|
||||
from configs import COMMANDS, ImportantID
|
||||
from middleware.loggers import log
|
||||
|
||||
# user_id -> thread_id (топик пользователя)
|
||||
user_topic_map: Dict[int, int] = {}
|
||||
# message_id в топике -> user_id
|
||||
topic_message_map: Dict[int, int] = {}
|
||||
|
||||
__all__ = ("router", "user_topic_map")
|
||||
CMD: str = "new"
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
STATE_WAITING_REQUEST = "waiting_request"
|
||||
|
||||
|
||||
def has_active_topic(user_id: int) -> bool:
|
||||
"""Проверяет, есть ли у пользователя активный топик"""
|
||||
return user_id in user_topic_map
|
||||
|
||||
|
||||
async def send_topic_message(user_id: int, text: str, reply_markup=None):
|
||||
"""Отправляет сообщение в топик пользователя"""
|
||||
thread_id = user_topic_map.get(user_id)
|
||||
if not thread_id:
|
||||
return False
|
||||
|
||||
try:
|
||||
await BotInfo.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=text,
|
||||
parse_mode="HTML",
|
||||
reply_markup=reply_markup
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
log(level='ERROR', log_type='TOPIC_SEND', text=f"Ошибка отправки в топик: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ===================== Продолжение диалога =====================
|
||||
@router.callback_query(F.data == "continue_dialog")
|
||||
async def continue_dialog_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик продолжения существующего диалога"""
|
||||
user_id = callback.from_user.id
|
||||
|
||||
if not has_active_topic(user_id):
|
||||
await callback.answer("❌ Активный диалог не найден", show_alert=True)
|
||||
return
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await callback.message.edit_text(
|
||||
text="💬 У вас уже есть активный диалог с поддержкой. Просто отправьте ваше сообщение (не через reply) и оно будет переслано администратору.",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ===================== Обработчик callback /new =====================
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@log(level='INFO', log_type=f"{CMD.upper()}_CBD", text=f"использовал команду /{CMD} через кнопку")
|
||||
async def new_cmd_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /new из callback кнопки"""
|
||||
user_id = callback.from_user.id
|
||||
|
||||
# Проверяем, есть ли уже активный топик
|
||||
if has_active_topic(user_id):
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
|
||||
ikb.row(InlineKeyboardButton(text="Создать новый📝", callback_data='force_new'))
|
||||
|
||||
await callback.message.edit_text(
|
||||
text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n"
|
||||
"• <b>Продолжить текущий</b> - чтобы писать в существующий диалог\n"
|
||||
"• <b>Создать новый</b> - если хотите начать новый запрос (старый диалог будет архивирован)",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
return
|
||||
|
||||
await status_clear(message=callback.message, state=state)
|
||||
await state.set_state(STATE_WAITING_REQUEST)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
text="Отправьте свой запрос:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
except Exception:
|
||||
await callback.message.answer(
|
||||
text="Отправьте свой запрос:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ===================== Принудительное создание нового топика =====================
|
||||
@router.callback_query(F.data == "force_new")
|
||||
async def force_new_callback(callback: CallbackQuery, state: FSMContext) -> None:
|
||||
"""Принудительное создание нового топика (при наличии активного)"""
|
||||
user_id = callback.from_user.id
|
||||
|
||||
# Уведомляем в старом топике о создании нового
|
||||
if has_active_topic(user_id):
|
||||
await send_topic_message(
|
||||
user_id,
|
||||
f"🔔 <b>Пользователь начал новый запрос</b>\n"
|
||||
f"Старый топик будет архивирован."
|
||||
)
|
||||
# Не удаляем старый топик из мапы сразу - он перезапишется при создании нового
|
||||
|
||||
await status_clear(message=callback.message, state=state)
|
||||
await state.set_state(STATE_WAITING_REQUEST)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await callback.message.edit_text(
|
||||
text="📝 <b>Создание нового запроса</b>\n\nОтправьте ваш запрос:",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
# ===================== Обработчик сообщения /new =====================
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type=CMD.upper(), text=f"использовал команду /{CMD}")
|
||||
async def new_cmd_message(message: Message, state: FSMContext) -> None:
|
||||
"""Обработчик команды /new из текстового сообщения"""
|
||||
user_id = message.from_user.id
|
||||
|
||||
# Проверяем, есть ли уже активный топик
|
||||
if has_active_topic(user_id):
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
|
||||
|
||||
await message.answer(
|
||||
text="⚠️ У вас уже есть активный диалог с поддержкой.\n\n"
|
||||
"Используйте кнопку ниже чтобы продолжить общение в существующем диалоге.",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
return
|
||||
|
||||
await status_clear(message=message, state=state)
|
||||
await state.set_state(STATE_WAITING_REQUEST)
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Отмена↩️", callback_data='start'))
|
||||
|
||||
await message.answer(
|
||||
text="Отправьте свой запрос:",
|
||||
reply_markup=ikb.as_markup()
|
||||
)
|
||||
|
||||
|
||||
# ===================== Создание топика и отправка запроса =====================
|
||||
@router.message(StateFilter(STATE_WAITING_REQUEST))
|
||||
async def process_request(message: Message, state: FSMContext) -> None:
|
||||
"""Создание топика и отправка запроса пользователя"""
|
||||
text = message.text.strip()
|
||||
if not text:
|
||||
await message.reply("⚠️ Пожалуйста, отправьте непустое сообщение.")
|
||||
return
|
||||
|
||||
user = message.from_user
|
||||
|
||||
try:
|
||||
# Создаем новый топик для пользователя
|
||||
topic_name = f"👤 {user.full_name} (ID: {user.id})"
|
||||
topic_result = await message.bot.create_forum_topic(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
name=topic_name
|
||||
)
|
||||
|
||||
thread_id = topic_result.message_thread_id
|
||||
|
||||
# Отправляем сообщение пользователя в новый топик
|
||||
formatted_text = f"<b>📩 Сообщение от <a href='tg://user?id={user.id}'>{user.full_name}</a>:</b>\n{text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=formatted_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Отправляем сообщение с уведомлением (со звуком)
|
||||
await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text="🔔 <b>Новый запрос создан</b>\nАдминистратор уведомлен.",
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
# Сохраняем связь пользователя и топика
|
||||
user_topic_map[user.id] = thread_id
|
||||
topic_message_map[sent_msg.message_id] = user.id
|
||||
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Перейти к диалогу💬", callback_data='continue_dialog'))
|
||||
ikb.row(InlineKeyboardButton(text="В меню↩️", callback_data='start'))
|
||||
|
||||
await message.answer(
|
||||
text="✅ <b>Запрос отправлен!</b>\n\n"
|
||||
"Администратор ответит в этом боте. Вы можете продолжить общение через меню.",
|
||||
reply_markup=ikb.as_markup(),
|
||||
parse_mode="HTML"
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
except Exception as e:
|
||||
await message.reply(f"⚠️ Не удалось создать запрос: {e}")
|
||||
|
||||
|
||||
# ===================== Пересылка сообщений пользователя в топик =====================
|
||||
@router.message(F.chat.type == "private", ~F.reply_to_message)
|
||||
async def forward_user_to_admin(message: Message) -> None:
|
||||
"""Пересылает сообщения пользователя в топик (если есть активный диалог)"""
|
||||
if message.from_user.is_bot:
|
||||
return
|
||||
|
||||
user_id = message.from_user.id
|
||||
|
||||
# Проверяем, есть ли активный топик
|
||||
if not has_active_topic(user_id):
|
||||
return # Нет активного топика - игнорируем
|
||||
|
||||
# Получаем топик пользователя
|
||||
thread_id = user_topic_map.get(user_id)
|
||||
if not thread_id:
|
||||
return
|
||||
|
||||
try:
|
||||
# Отправляем сообщение пользователя в топик
|
||||
if message.text:
|
||||
formatted_text = f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=formatted_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
elif message.photo:
|
||||
caption = f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Сообщение от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
|
||||
sent_msg = await message.bot.send_photo(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
await message.answer("✅ Сообщение отправлено администратору")
|
||||
|
||||
except Exception as e:
|
||||
await message.answer(f"⚠️ Не удалось отправить сообщение: {e}")
|
||||
|
||||
|
||||
# ===================== Пересылка ответов админа пользователю =====================
|
||||
@router.message(F.chat.id == ImportantID.SUPPORT_CHAT_ID, F.message_thread_id)
|
||||
async def forward_admin_to_user(message: Message) -> None:
|
||||
"""Пересылает сообщения админа из топика пользователю"""
|
||||
if message.from_user.is_bot:
|
||||
return
|
||||
|
||||
thread_id = message.message_thread_id
|
||||
|
||||
# Ищем пользователя по thread_id топика
|
||||
user_id = None
|
||||
for uid, tid in user_topic_map.items():
|
||||
if tid == thread_id:
|
||||
user_id = uid
|
||||
break
|
||||
|
||||
if not user_id:
|
||||
return # Не наш топик
|
||||
|
||||
try:
|
||||
# Пересылаем сообщение админа пользователю
|
||||
if message.text:
|
||||
text = f"<b>👨💼 Ответ администратора:</b>\n{message.html_text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=user_id,
|
||||
text=text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
# Сохраняем связь для возможного ответа пользователя
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
elif message.photo:
|
||||
caption = f"<b>👨💼 Ответ администратора:</b>\n{message.html_text}" if message.caption else "<b>👨💼 Ответ администратора:</b>"
|
||||
await message.bot.send_photo(
|
||||
chat_id=user_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log(level='ERROR', log_type='FORWARD', text=f"Ошибка пересылки админ->пользователь: {e}")
|
||||
|
||||
|
||||
# ===================== Пересылка ответов пользователя в топик =====================
|
||||
@router.message(F.chat.type == "private", F.reply_to_message)
|
||||
async def forward_user_reply_to_admin(message: Message) -> None:
|
||||
"""Пересылает ответы пользователя (reply) в топик"""
|
||||
if message.from_user.is_bot:
|
||||
return
|
||||
|
||||
user_id = message.from_user.id
|
||||
reply_to_id = message.reply_to_message.message_id
|
||||
|
||||
# Проверяем, является ли это ответом на сообщение из топика
|
||||
original_user_id = topic_message_map.get(reply_to_id)
|
||||
if not original_user_id or original_user_id != user_id:
|
||||
return
|
||||
|
||||
# Получаем топик пользователя
|
||||
thread_id = user_topic_map.get(user_id)
|
||||
if not thread_id:
|
||||
await message.reply("⚠️ Не найден активный диалог. Используйте /new для нового запроса.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Отправляем ответ пользователя в топик
|
||||
if message.text:
|
||||
formatted_text = f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}"
|
||||
sent_msg = await message.bot.send_message(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
text=formatted_text,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
elif message.photo:
|
||||
caption = f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>\n{message.html_text}" if message.caption else f"<b>💬 Ответ от <a href='tg://user?id={user_id}'>{message.from_user.full_name}</a>:</b>"
|
||||
sent_msg = await message.bot.send_photo(
|
||||
chat_id=ImportantID.SUPPORT_CHAT_ID,
|
||||
message_thread_id=thread_id,
|
||||
photo=message.photo[-1].file_id,
|
||||
caption=caption,
|
||||
parse_mode="HTML"
|
||||
)
|
||||
topic_message_map[sent_msg.message_id] = user_id
|
||||
|
||||
await message.reply("✅ Ответ отправлен администратору.")
|
||||
|
||||
except Exception as e:
|
||||
await message.reply(f"⚠️ Не удалось отправить ответ: {e}")
|
||||
68
bot/handlers/commands/users/start_cmd.py
Normal file
68
bot/handlers/commands/users/start_cmd.py
Normal file
@@ -0,0 +1,68 @@
|
||||
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.templates import msg_photo
|
||||
from configs import COMMANDS, RpValue
|
||||
from .new_cmd import user_topic_map # Импортируем мапу топиков из модуля new
|
||||
|
||||
__all__ = ("router",)
|
||||
CMD: str = "start".casefold()
|
||||
router: Router = Router(name=f"{CMD}_cmd_router")
|
||||
|
||||
|
||||
@router.callback_query(F.data.casefold() == CMD)
|
||||
@router.message(Command(*COMMANDS[CMD], prefix=BotInfo.prefix, ignore_case=True))
|
||||
async def start_cmd(update: Message | CallbackQuery, state: FSMContext) -> None:
|
||||
"""Обработчик команды /start"""
|
||||
# Определяем тип update
|
||||
if isinstance(update, CallbackQuery):
|
||||
message = update.message
|
||||
callback = update
|
||||
else:
|
||||
message = update
|
||||
callback = None
|
||||
|
||||
# Проверяем, есть ли у пользователя активный топик
|
||||
user_id = update.from_user.id
|
||||
has_active_topic = user_id in user_topic_map
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Википедия🌐", url="https://t.me/PrimoWiki"))
|
||||
|
||||
if has_active_topic:
|
||||
# Если есть активный топик, показываем кнопку "Продолжить диалог"
|
||||
ikb.row(InlineKeyboardButton(text="Продолжить диалог💬", callback_data='continue_dialog'))
|
||||
else:
|
||||
# Если нет активного топика, показываем кнопку "Связаться"
|
||||
ikb.row(InlineKeyboardButton(text="Связаться👀", callback_data='new'))
|
||||
|
||||
# Формируем приветственное сообщение
|
||||
text: str = _(
|
||||
"""Добро пожаловать, <a href="{url}">{name}</a>!
|
||||
|
||||
Я ваш помощник по проекту — <b>PrimoWiki</b>!
|
||||
Моя цель — помочь вам сориентироваться и сделать ваше вступление куда проще!
|
||||
|
||||
Надеюсь, я смогу вам помочь! Пожалуйста, выберите нужную функцию на клавиатуре!
|
||||
"""
|
||||
).format(
|
||||
url=update.from_user.url,
|
||||
name=update.from_user.first_name,
|
||||
rp_name=RpValue.RP_NAME,
|
||||
)
|
||||
|
||||
# Добавляем информацию об активном диалоге, если есть
|
||||
if has_active_topic:
|
||||
text += "\n\n💬 <b>У вас есть активный диалог с поддержкой!</b>"
|
||||
|
||||
# Отправляем сообщение
|
||||
await msg_photo(message=message, text=text, file=f'assets/{CMD}.jpg', markup=ikb)
|
||||
|
||||
if callback:
|
||||
await callback.answer()
|
||||
58
bot/handlers/commands/users/union_cmd.py
Normal file
58
bot/handlers/commands/users/union_cmd.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# bot/handlers/commands/union_cmd.py
|
||||
from aiogram import Router, F
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message, CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from aiogram.fsm.context import FSMContext
|
||||
|
||||
from bot.core import BotInfo
|
||||
from bot.states.union_states import UnionStates
|
||||
from bot.templates import msg
|
||||
from middleware import log
|
||||
|
||||
# Настройка экспорта и роутера
|
||||
__all__ = ("router",)
|
||||
|
||||
|
||||
router: Router = Router(name="union_cmd_router")
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data == "union")
|
||||
@router.message(Command('union','гтшщт', 'союз', prefix=BotInfo.prefix, ignore_case=True))
|
||||
@log(level='INFO', log_type='Start', text="использовал(а) команду /union")
|
||||
async def create_cmd(message: Message|CallbackQuery, state: FSMContext) -> None:
|
||||
"""
|
||||
Обработчик команды /union.
|
||||
"""
|
||||
# Сбросим все состояния (отменим создание поста, если оно было)
|
||||
#await state.clear()
|
||||
|
||||
# Создание инлайн-клавиатуры
|
||||
ikb: InlineKeyboardBuilder = InlineKeyboardBuilder()
|
||||
ikb.row(InlineKeyboardButton(text="Правила❗️", url='https://teletype.in/@velli_arsaan/XxUiHcB4Puj'))
|
||||
ikb.row(InlineKeyboardButton(text="Назад↪️", callback_data='start'))
|
||||
|
||||
# Создание базовых переменных сообщения
|
||||
caption: str = f"""
|
||||
Приветствуем! Это бот для связи по вопросам союзов проекта ˚₊· ➳ 𝑆𝑦𝑠𝑡𝑒𝑚 𝑅𝑒𝑠𝑒𝑡 ·₊˚.
|
||||
Задайте свой вопрос, и мы постараемся ответить вам в ближайшее время — в некотором случае можем попроосить вас дать юз/ссылку на ваш проект.
|
||||
|
||||
Предложение о заключении союзов должно выглядеть вот так:
|
||||
– Название
|
||||
– Юз и ссылка на инфо
|
||||
– Юз и ссылка на лайф
|
||||
– Условия союзов
|
||||
– Юзер следящего с вашей стороны
|
||||
– Желаемый следящий с нашей стороны (мы будем в праве поставить вам другого, но тот, которого вы назовёте, будет в приоритете)
|
||||
– Кодовое предложение из условий союзов. Оно состоит из 4 слов, которые расположены в верном порядке в статье о наших условиях сотрудничества.
|
||||
|
||||
Имейте ввиду, что мы можем отказаться от союза без объяснения причин!
|
||||
"""
|
||||
|
||||
# Обработчик ответа на сообщение
|
||||
await msg(message=message, text=caption, markup=ikb)
|
||||
|
||||
# Установим состояние ожидания анкеты
|
||||
await state.set_state(UnionStates.waiting_for_union)
|
||||
Reference in New Issue
Block a user