First commit

This commit is contained in:
2026-01-23 04:45:55 +07:00
commit 0b251c5967
118 changed files with 9580 additions and 0 deletions

View 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,
)

View 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,
)

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

View 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("Не удалось получить список забаненных пользователей")

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

View File

View 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("✅ Сообщение закреплено")

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

View File

View 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,
)

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

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

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

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

View File

@@ -0,0 +1,9 @@
from aiogram import Router
# Настройка экспорта и роутера
__all__ = ("router",)
router: Router = Router(name=__name__)
# Подключение роутеров
# router.include_routers(
# )

View 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,
)

View 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, )

View 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()

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

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

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

View 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()

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