1794 lines
68 KiB
Python
1794 lines
68 KiB
Python
from aiogram import types, Router, F
|
||
from aiogram.filters import Command
|
||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||
from core import db
|
||
from core.remnawave import remnawave
|
||
from keyboards import Keyboards
|
||
from keyboards.payment import PaymentKeyboards
|
||
from config import config
|
||
from locales import localization
|
||
|
||
# Роутер для основного меню
|
||
main_router = Router()
|
||
|
||
# Роутер для админ-панели
|
||
admin_router = Router()
|
||
|
||
|
||
# Временное хранилище для тарифов пользователей (user_id -> tariff_info)
|
||
payment_temp = {}
|
||
|
||
# Временное хранилище для создания тикетов (user_id -> True)
|
||
ticket_creation = {}
|
||
|
||
# Временное хранилище для дополнения тикетов (user_id -> ticket_id)
|
||
ticket_append_mode = {}
|
||
|
||
|
||
def is_admin(user_id: int) -> bool:
|
||
"""Проверка, является ли пользователь администратором"""
|
||
return user_id in config.admin_ids
|
||
|
||
|
||
async def get_user_locale(user_id: int) -> str:
|
||
"""Получение языка пользователя из БД"""
|
||
return await db.get_user_language(user_id)
|
||
|
||
|
||
@main_router.message(Command("start"))
|
||
async def handle_start(message: types.Message):
|
||
"""Обработчик команды /start"""
|
||
user_id = message.from_user.id
|
||
username = message.from_user.username or message.from_user.first_name
|
||
locale = await get_user_locale(user_id)
|
||
|
||
# Создаём пользователя в БД если нет
|
||
await db.create_user(user_id, username)
|
||
|
||
# Проверяем статус подписки
|
||
is_active = await db.get_user_subscription(user_id)
|
||
|
||
subscription_status = (
|
||
localization.get("subscription_active", locale) if is_active
|
||
else localization.get("subscription_inactive", locale)
|
||
)
|
||
|
||
# Формируем сообщение
|
||
text = (
|
||
f"{localization.get('welcome', locale, username=username)}\n\n"
|
||
f"{localization.get('subscription_status', locale, status=subscription_status)}\n\n"
|
||
f"{localization.get('select_action', locale)}"
|
||
)
|
||
|
||
# Проверяем, является ли пользователь администратором
|
||
if is_admin(user_id):
|
||
# Добавляем кнопку админ-панели
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="💳 Купить подписку",
|
||
callback_data="buy_subscription"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="📞 Техподдержка",
|
||
callback_data="support"
|
||
),
|
||
InlineKeyboardButton(
|
||
text="📜 Правила сервиса",
|
||
callback_data="rules"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🌐 Язык",
|
||
callback_data="language"
|
||
),
|
||
InlineKeyboardButton(
|
||
text="⚙️ Админ",
|
||
callback_data="admin_panel"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
else:
|
||
keyboard = Keyboards.get_main_keyboard(locale)
|
||
|
||
await message.answer(
|
||
text,
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "buy_subscription")
|
||
async def handle_callback_buy_subscription(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Купить подписку'"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
subscription_title = localization.get("subscription.standard_title", locale)
|
||
subscription_desc = localization.get("subscription.standard_description", locale)
|
||
|
||
await callback.message.edit_text(
|
||
f"{subscription_title}\n\n{subscription_desc}",
|
||
reply_markup=Keyboards.get_buy_subscription_keyboard(locale)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "back_to_buy")
|
||
async def handle_callback_back_to_buy(callback: types.CallbackQuery):
|
||
"""Возврат к выбору тарифа"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
subscription_title = localization.get("subscription.standard_title", locale)
|
||
subscription_desc = localization.get("subscription.standard_description", locale)
|
||
|
||
await callback.message.edit_text(
|
||
f"{subscription_title}\n\n{subscription_desc}",
|
||
reply_markup=Keyboards.get_buy_subscription_keyboard(locale)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "support")
|
||
async def handle_callback_support(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Техподдержка'"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
support_title = localization.get("messages.support_title", locale)
|
||
support_desc = localization.get("messages.support_description", locale)
|
||
|
||
await callback.message.edit_text(
|
||
f"{support_title}\n\n{support_desc}",
|
||
reply_markup=Keyboards.get_support_keyboard(locale)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "support_create_ticket")
|
||
async def handle_support_create_ticket(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Создать тикет'"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
# Устанавливаем флаг, что пользователь создаёт тикет
|
||
ticket_creation[user_id] = True
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data="cancel_ticket"
|
||
),
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_support"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
"📝 Создание тикета\n\n"
|
||
"Опишите вашу проблему или вопрос одним сообщением.\n\n"
|
||
"Пример: 'Не работает VPN, ошибка подключения'\n\n"
|
||
"Отправьте сообщение ниже или нажмите 'Отмена':",
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "cancel_ticket")
|
||
async def handle_cancel_ticket(callback: types.CallbackQuery):
|
||
"""Отмена создания тикета"""
|
||
user_id = callback.from_user.id
|
||
|
||
# Удаляем флаг создания тикета
|
||
if user_id in ticket_creation:
|
||
del ticket_creation[user_id]
|
||
|
||
locale = await get_user_locale(user_id)
|
||
|
||
await callback.message.edit_text(
|
||
"❌ Создание тикета отменено.\n\n"
|
||
"Если у вас возникнут вопросы, вы всегда можете создать тикет позже.",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_support"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "support_my_tickets")
|
||
async def handle_support_my_tickets(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Мои тикеты'"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
# Получаем тикеты пользователя из БД
|
||
tickets = await db.get_user_tickets(user_id)
|
||
|
||
if not tickets:
|
||
await callback.message.edit_text(
|
||
"📋 Мои тикеты\n\n"
|
||
"У вас пока нет активных тикетов.\n\n"
|
||
"Создайте новый тикет, если у вас есть вопрос или проблема.",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_support"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
else:
|
||
# Формируем список тикетов
|
||
tickets_text = "📋 Мои тикеты\n\n"
|
||
|
||
for ticket in tickets[:5]: # Показываем последние 5 тикетов
|
||
status_emoji = "🟢" if ticket['status'] == 'open' else "🔵" if ticket['status'] == 'answered' else "⚫"
|
||
created_at = ticket['created_at'].strftime("%d.%m.%Y %H:%M") if ticket['created_at'] else 'Неизвестно'
|
||
|
||
tickets_text += (
|
||
f"{status_emoji} Тикет #{ticket['id']} - {ticket['status']}\n"
|
||
f"Дата: {created_at}\n"
|
||
f"Вопрос: {ticket['message'][:50]}...\n\n"
|
||
)
|
||
|
||
if len(tickets) > 5:
|
||
tickets_text += f"... и ещё {len(tickets) - 5} тикетов\n"
|
||
|
||
# Клавиатура с кнопками тикетов
|
||
ticket_buttons = []
|
||
for ticket in tickets[:5]:
|
||
status_emoji = "🟢" if ticket['status'] == 'open' else "🔵" if ticket['status'] == 'answered' else "⚫"
|
||
ticket_buttons.append([
|
||
InlineKeyboardButton(
|
||
text=f"{status_emoji} #{ticket['id']} - {ticket['status']}",
|
||
callback_data=f"ticket_view_{ticket['id']}"
|
||
)
|
||
])
|
||
ticket_buttons.append([
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_support"
|
||
)
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
tickets_text,
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=ticket_buttons)
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("ticket_view_"))
|
||
async def handle_ticket_view(callback: types.CallbackQuery):
|
||
"""Просмотр тикета"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
ticket_id = int(callback.data.replace("ticket_view_", ""))
|
||
await show_ticket_view(callback, user_id, locale, ticket_id)
|
||
|
||
|
||
async def show_ticket_view(callback, user_id, locale, ticket_id):
|
||
"""Отображение просмотра тикета"""
|
||
ticket = await db.get_ticket(ticket_id)
|
||
|
||
if not ticket or ticket['user_id'] != user_id:
|
||
await callback.answer("Тикет не найден", show_alert=True)
|
||
return
|
||
|
||
# Статус тикета
|
||
status_text = {
|
||
'open': '🟢 Открыт',
|
||
'answered': '🔵 Дан ответ',
|
||
'closed': '⚫ Закрыт'
|
||
}.get(ticket['status'], ticket['status'])
|
||
|
||
# Формируем сообщение
|
||
ticket_text = f"📋 Тикет #{ticket['id']}\n\n"
|
||
ticket_text += f"Статус: {status_text}\n"
|
||
ticket_text += f"Дата создания: {ticket['created_at'].strftime('%d.%m.%Y %H:%M') if ticket['created_at'] else 'Неизвестно'}\n\n"
|
||
ticket_text += f"❓ Ваш вопрос:\n{ticket['message']}\n\n"
|
||
|
||
if ticket['admin_response']:
|
||
ticket_text += f"💬 Ответ поддержки:\n{ticket['admin_response']}\n\n"
|
||
|
||
# Информация о закрытии
|
||
if ticket['status'] == 'closed':
|
||
closed_by = ticket.get('closed_by', 'Неизвестно')
|
||
closed_at = ticket.get('closed_at')
|
||
closed_info = f"🔒 Закрыл: {closed_by}"
|
||
if closed_at:
|
||
closed_info += f"\n🕒 Дата: {closed_at.strftime('%d.%m.%Y %H:%M') if hasattr(closed_at, 'strftime') else closed_at}"
|
||
ticket_text += f"{closed_info}\n\n"
|
||
|
||
if not ticket['admin_response'] and ticket['status'] != 'closed':
|
||
ticket_text += "⏳ Ожидайте ответа поддержки...\n\n"
|
||
|
||
# Кнопки
|
||
keyboard_buttons = [
|
||
[
|
||
InlineKeyboardButton(
|
||
text="✏️ Дополнить тикет",
|
||
callback_data=f"ticket_append_{ticket['id']}"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="support_my_tickets"
|
||
)
|
||
]
|
||
]
|
||
|
||
# Если тикет открыт, можно закрыть
|
||
if ticket['status'] == 'open':
|
||
keyboard_buttons.insert(0, [
|
||
InlineKeyboardButton(
|
||
text="✅ Закрыть тикет",
|
||
callback_data=f"ticket_close_{ticket['id']}"
|
||
)
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
ticket_text,
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("ticket_close_"))
|
||
async def handle_ticket_close(callback: types.CallbackQuery):
|
||
"""Закрытие тикета"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
username = callback.from_user.username or callback.from_user.first_name
|
||
|
||
ticket_id = int(callback.data.replace("ticket_close_", ""))
|
||
ticket = await db.get_ticket(ticket_id)
|
||
|
||
if not ticket or ticket['user_id'] != user_id:
|
||
await callback.answer("Тикет не найден", show_alert=True)
|
||
return
|
||
|
||
# Закрываем тикет с указанием кто закрыл (передаём позиционно, не именованно)
|
||
await db.close_ticket(ticket_id, f"@{username}" if callback.from_user.username else f"{username}")
|
||
|
||
await callback.answer("Тикет закрыт")
|
||
|
||
# Обновляем сообщение с информацией кто закрыл (передаём ticket_id)
|
||
try:
|
||
await handle_ticket_view_by_id(callback, ticket_id)
|
||
except Exception as e:
|
||
if "message is not modified" not in str(e):
|
||
print(f"Ошибка обновления сообщения: {e}")
|
||
|
||
|
||
async def handle_ticket_view_by_id(callback, ticket_id):
|
||
"""Просмотр тикета по ID (для вызова из других функций)"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
await show_ticket_view(callback, user_id, locale, ticket_id)
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("ticket_append_"))
|
||
async def handle_ticket_append(callback: types.CallbackQuery):
|
||
"""Режим дополнения тикета"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
ticket_id = int(callback.data.replace("ticket_append_", ""))
|
||
ticket = await db.get_ticket(ticket_id)
|
||
|
||
if not ticket or ticket['user_id'] != user_id:
|
||
await callback.answer("Тикет не найден", show_alert=True)
|
||
return
|
||
|
||
# Устанавливаем режим дополнения
|
||
ticket_append_mode[user_id] = ticket_id
|
||
|
||
await callback.message.edit_text(
|
||
f"✏️ Дополнение тикета #{ticket_id}\n\n"
|
||
f"Напишите дополнение к вашему тикету.\n\n"
|
||
f"Текущий статус: {ticket['status']}\n\n"
|
||
f"Отправьте сообщение ниже или нажмите 'Отмена':",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data=f"ticket_append_cancel_{ticket_id}"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("ticket_append_cancel_"))
|
||
async def handle_ticket_append_cancel(callback: types.CallbackQuery):
|
||
"""Отмена дополнения тикета"""
|
||
user_id = callback.from_user.id
|
||
|
||
# Удаляем из режима дополнения
|
||
if user_id in ticket_append_mode:
|
||
del ticket_append_mode[user_id]
|
||
|
||
# Возвращаемся к просмотру тикета
|
||
ticket_id = int(callback.data.replace("ticket_append_cancel_", ""))
|
||
|
||
# Обновляем сообщение
|
||
await handle_ticket_view(callback)
|
||
await callback.answer("Отменено")
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "back_to_support")
|
||
async def handle_callback_back_to_support(callback: types.CallbackQuery):
|
||
"""Возврат в меню поддержки"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
support_title = localization.get("messages.support_title", locale)
|
||
support_desc = localization.get("messages.support_description", locale)
|
||
|
||
await callback.message.edit_text(
|
||
f"{support_title}\n\n{support_desc}",
|
||
reply_markup=Keyboards.get_support_keyboard(locale)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "rules")
|
||
async def handle_callback_rules(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Правила сервиса'"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
rules_title = localization.get("messages.rules_title", locale)
|
||
rules_text = localization.get("messages.rules_text", locale)
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
f"{rules_title}\n\n{rules_text}",
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "language")
|
||
async def handle_callback_language(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Язык'"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
await callback.message.edit_text(
|
||
localization.get("messages.select_language", locale),
|
||
reply_markup=Keyboards.get_language_keyboard(locale)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data == "back_to_main")
|
||
async def handle_callback_back_to_main(callback: types.CallbackQuery):
|
||
"""Возврат в главное меню"""
|
||
user_id = callback.from_user.id
|
||
username = callback.from_user.username or callback.from_user.first_name
|
||
locale = await get_user_locale(user_id)
|
||
|
||
is_active = await db.get_user_subscription(user_id)
|
||
|
||
subscription_status = (
|
||
localization.get("subscription_active", locale) if is_active
|
||
else localization.get("subscription_inactive", locale)
|
||
)
|
||
|
||
text = (
|
||
f"{localization.get('welcome', locale, username=username)}\n\n"
|
||
f"{localization.get('subscription_status', locale, status=subscription_status)}\n\n"
|
||
f"{localization.get('select_action', locale)}"
|
||
)
|
||
|
||
# Проверяем, является ли пользователь администратором
|
||
if is_admin(user_id):
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="💳 Купить подписку",
|
||
callback_data="buy_subscription"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="📞 Техподдержка",
|
||
callback_data="support"
|
||
),
|
||
InlineKeyboardButton(
|
||
text="📜 Правила сервиса",
|
||
callback_data="rules"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🌐 Язык",
|
||
callback_data="language"
|
||
),
|
||
InlineKeyboardButton(
|
||
text="⚙️ Админ",
|
||
callback_data="admin_panel"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
else:
|
||
keyboard = Keyboards.get_main_keyboard(locale)
|
||
|
||
await callback.message.edit_text(
|
||
text=text,
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("lang_"))
|
||
async def handle_callback_language_change(callback: types.CallbackQuery):
|
||
"""Обработчик смены языка"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
lang = callback.data.replace("lang_", "")
|
||
|
||
# Сохраняем язык в БД
|
||
await db.set_user_language(user_id, lang)
|
||
|
||
await callback.answer(
|
||
localization.get("messages.language_changed", locale, language=localization.get_locale_name(lang))
|
||
)
|
||
|
||
# Возвращаемся в главное меню
|
||
await handle_callback_back_to_main(callback)
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data in ["buy_14_days", "buy_30_days", "buy_60_days", "buy_90_days", "buy_180_days", "buy_360_days"])
|
||
async def handle_callback_buy_tariff(callback: types.CallbackQuery):
|
||
"""Обработчик выбора тарифа"""
|
||
user_id = callback.from_user.id
|
||
locale = await get_user_locale(user_id)
|
||
|
||
# Карта тарифов: callback_data -> (days, price)
|
||
tariffs_map = {
|
||
"buy_14_days": (14, "150₽"),
|
||
"buy_30_days": (30, "300₽"),
|
||
"buy_60_days": (60, "600₽"),
|
||
"buy_90_days": (90, "900₽"),
|
||
"buy_180_days": (180, "1800₽"),
|
||
"buy_360_days": (360, "3600₽")
|
||
}
|
||
|
||
tariff_info = tariffs_map.get(callback.data)
|
||
if tariff_info:
|
||
days, price = tariff_info
|
||
# Сохраняем тариф во временное хранилище
|
||
payment_temp[user_id] = {"days": days, "price": price, "tariff": callback.data}
|
||
|
||
payment_title = localization.get("messages.payment_instruction_title", locale)
|
||
payment_text = localization.get("messages.payment_instruction_text", locale, phone=config.payment_phone)
|
||
|
||
# Клавиатура с кнопкой "Назад"
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_buy"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
f"{payment_title}\n\n{payment_text}",
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.message(F.photo)
|
||
async def handle_payment_photo(message: types.Message):
|
||
"""Обработчик фото чека"""
|
||
user_id = message.from_user.id
|
||
username = message.from_user.username or message.from_user.first_name
|
||
|
||
# Получаем информацию о тарифе из временного хранилища
|
||
tariff_info = payment_temp.get(user_id, {})
|
||
days = tariff_info.get("days", 0)
|
||
tariff = tariff_info.get("tariff", "unknown")
|
||
|
||
# Отправляем фото в канал (в тему 5910) с кнопками аппрува
|
||
if config.payment_channel_id:
|
||
try:
|
||
await message.bot.copy_message(
|
||
chat_id=config.payment_channel_id,
|
||
from_chat_id=message.chat.id,
|
||
message_id=message.message_id,
|
||
caption=f"💳 Оплата от @{username} ({user_id})\nДней: {days}\nТариф: {tariff}\n{message.caption or ''}",
|
||
message_thread_id=5910,
|
||
reply_markup=PaymentKeyboards.get_payment_approval_keyboard(user_id, tariff, days)
|
||
)
|
||
print(f"✅ Фото отправлено в канал {config.payment_channel_id}, тема 5910")
|
||
except Exception as e:
|
||
print(f"❌ Ошибка отправки фото в канал: {e}")
|
||
|
||
# Отвечаем пользователю с кнопкой "В главное меню"
|
||
locale = await get_user_locale(user_id)
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await message.answer(
|
||
localization.get("messages.payment_receipt_sent", locale),
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
|
||
@main_router.message(F.text)
|
||
async def handle_ticket_message(message: types.Message):
|
||
"""Обработчик текстовых сообщений для создания/дополнения тикета"""
|
||
user_id = message.from_user.id
|
||
username = message.from_user.username or message.from_user.first_name
|
||
text = message.text
|
||
|
||
# Проверяем, отвечает ли админ на тикет
|
||
admin_reply_mode = ticket_append_mode.get(user_id)
|
||
if admin_reply_mode and isinstance(admin_reply_mode, str) and admin_reply_mode.startswith("admin_reply_"):
|
||
ticket_id = int(admin_reply_mode.replace("admin_reply_", ""))
|
||
|
||
# Сохраняем ответ в БД
|
||
await db.update_ticket_response(ticket_id, text)
|
||
|
||
# Отправляем ответ в канал
|
||
ticket_channel_id = config.payment_ticket_channel_id or config.payment_channel_id
|
||
ticket_thread_id = config.payment_ticket_thread_id
|
||
|
||
if ticket_channel_id:
|
||
try:
|
||
if ticket_thread_id:
|
||
await message.bot.send_message(
|
||
chat_id=ticket_channel_id,
|
||
text=f"💬 Ответ администратора в тикет #{ticket_id}:\n\n{text}",
|
||
message_thread_id=int(ticket_thread_id)
|
||
)
|
||
else:
|
||
await message.bot.send_message(
|
||
chat_id=ticket_channel_id,
|
||
text=f"💬 Ответ администратора в тикет #{ticket_id}:\n\n{text}"
|
||
)
|
||
print(f"✅ Ответ админа отправлен в тикет #{ticket_id}")
|
||
except Exception as e:
|
||
print(f"Ошибка отправки ответа админа: {e}")
|
||
|
||
# Отвечаем пользователю
|
||
try:
|
||
ticket = await db.get_ticket(ticket_id)
|
||
if ticket:
|
||
await message.bot.send_message(
|
||
chat_id=ticket['user_id'],
|
||
text=f"📋 Ответ поддержки в вашем тикете #{ticket_id}:\n\n{text}"
|
||
)
|
||
except Exception as e:
|
||
print(f"Не удалось отправить ответ пользователю: {e}")
|
||
|
||
# Удаляем режим ответа
|
||
del ticket_append_mode[user_id]
|
||
|
||
await message.answer(
|
||
f"✅ Ответ отправлен в тикет #{ticket_id}!\n\nПользователь получит уведомление.",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад в админку",
|
||
callback_data="admin_back"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
return
|
||
|
||
# Проверяем, создаёт ли пользователь тикет
|
||
if ticket_creation.get(user_id):
|
||
# Сохраняем тикет в БД
|
||
ticket_id = await db.create_ticket(user_id, username, text)
|
||
|
||
# Отправляем тикет в канал (с thread_id)
|
||
ticket_channel_id = config.payment_ticket_channel_id or config.payment_channel_id
|
||
ticket_thread_id = config.payment_ticket_thread_id
|
||
|
||
if ticket_channel_id:
|
||
try:
|
||
if ticket_thread_id:
|
||
# Отправляем в тему (форум)
|
||
await message.bot.send_message(
|
||
chat_id=ticket_channel_id,
|
||
text=f"📝 Новый тикет #{ticket_id} от @{username} ({user_id})\n\n❓ Вопрос:\n{text}",
|
||
message_thread_id=int(ticket_thread_id)
|
||
)
|
||
else:
|
||
# Отправляем просто в канал
|
||
await message.bot.send_message(
|
||
chat_id=ticket_channel_id,
|
||
text=f"📝 Новый тикет #{ticket_id} от @{username} ({user_id})\n\n❓ Вопрос:\n{text}"
|
||
)
|
||
print(f"✅ Тикет #{ticket_id} отправлен в канал {ticket_channel_id}, тема {ticket_thread_id}")
|
||
except Exception as e:
|
||
print(f"Ошибка отправки тикета в канал: {e}")
|
||
|
||
# Отвечаем пользователю
|
||
locale = await get_user_locale(user_id)
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
# Отправляем сообщение и сразу удаляем флаг
|
||
del ticket_creation[user_id] # Удаляем флаг ПЕРЕД отправкой
|
||
|
||
await message.answer(
|
||
f"✅ Тикет #{ticket_id} создан!\n\nНаша поддержка ответит вам в ближайшее время.\n\nОжидайте ответа в этом чате.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
# Проверяем, дополняет ли пользователь тикет
|
||
elif ticket_append_mode.get(user_id):
|
||
ticket_id = ticket_append_mode[user_id]
|
||
|
||
# Добавляем сообщение к тикету
|
||
await db.append_ticket_message(ticket_id, text)
|
||
|
||
# Отправляем уведомление в канал
|
||
ticket_channel_id = config.payment_ticket_channel_id or config.payment_channel_id
|
||
ticket_thread_id = config.payment_ticket_thread_id
|
||
|
||
if ticket_channel_id:
|
||
try:
|
||
if ticket_thread_id:
|
||
await message.bot.send_message(
|
||
chat_id=ticket_channel_id,
|
||
text=f"➕ Тикет #{ticket_id} дополнен от @{username} ({user_id})\n\n📝 Дополнение:\n{text}",
|
||
message_thread_id=int(ticket_thread_id)
|
||
)
|
||
else:
|
||
await message.bot.send_message(
|
||
chat_id=ticket_channel_id,
|
||
text=f"➕ Тикет #{ticket_id} дополнен от @{username} ({user_id})\n\n📝 Дополнение:\n{text}"
|
||
)
|
||
print(f"✅ Дополнение к тикуту #{ticket_id} отправлено в канал")
|
||
except Exception as e:
|
||
print(f"Ошибка отправки дополнения в канал: {e}")
|
||
|
||
# Отвечаем пользователю
|
||
locale = await get_user_locale(user_id)
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
# Удаляем режим дополнения
|
||
del ticket_append_mode[user_id]
|
||
|
||
await message.answer(
|
||
f"✅ Тикет #{ticket_id} дополнен!\n\nПоддержка увидит ваше сообщение.",
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
|
||
# ==================== АДМИН ПАНЕЛЬ ====================
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_panel")
|
||
async def handle_admin_panel_button(callback: types.CallbackQuery):
|
||
"""Админ-панель из кнопки"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="📋 Все тикеты",
|
||
callback_data="admin_all_tickets"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔍 Тикет по ID",
|
||
callback_data="admin_ticket_by_id"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="👥 Пользователи",
|
||
callback_data="admin_users"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="⚙️ Активировать подписку",
|
||
callback_data="admin_activate_sub"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
"⚙️ Админ-панель\n\nВыберите действие:",
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_users")
|
||
async def handle_admin_users(callback: types.CallbackQuery):
|
||
"""Управление пользователями - статистика"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
# Получаем статистику из БД
|
||
connection = db.get_connection()
|
||
cursor = connection.cursor(dictionary=True)
|
||
|
||
# Всего пользователей
|
||
cursor.execute("SELECT COUNT(*) as count FROM subscriptions")
|
||
total_users = cursor.fetchone()['count']
|
||
|
||
# Активных пользователей
|
||
cursor.execute("SELECT COUNT(*) as count FROM subscriptions WHERE is_active = TRUE")
|
||
active_users = cursor.fetchone()['count']
|
||
|
||
# Заблокированных (пока нет такой колонки, считаем неактивных)
|
||
blocked_users = total_users - active_users
|
||
|
||
# Новые пользователи сегодня
|
||
cursor.execute("""
|
||
SELECT COUNT(*) as count FROM subscriptions
|
||
WHERE DATE(created_at) = CURDATE()
|
||
""")
|
||
today_users = cursor.fetchone()['count']
|
||
|
||
# Новые пользователи за неделю
|
||
cursor.execute("""
|
||
SELECT COUNT(*) as count FROM subscriptions
|
||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||
""")
|
||
week_users = cursor.fetchone()['count']
|
||
|
||
# Новые пользователи за месяц
|
||
cursor.execute("""
|
||
SELECT COUNT(*) as count FROM subscriptions
|
||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
|
||
""")
|
||
month_users = cursor.fetchone()['count']
|
||
|
||
cursor.close()
|
||
connection.close()
|
||
|
||
stats_text = (
|
||
"👥 Управление пользователями\n\n"
|
||
"📊 Статистика:\n"
|
||
f"• Всего: {total_users}\n"
|
||
f"• Активных: {active_users}\n"
|
||
f"• Заблокированных: {blocked_users}\n\n"
|
||
"📈 Новые пользователи:\n"
|
||
f"• Сегодня: {today_users}\n"
|
||
f"• За неделю: {week_users}\n"
|
||
f"• За месяц: {month_users}"
|
||
)
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="<EFBFBD> Все пользователи",
|
||
callback_data="admin_users_list_1"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="<EFBFBD>📥 Импорт из Remnawave",
|
||
callback_data="admin_sync_remnawave"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад в админку",
|
||
callback_data="admin_back"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
stats_text,
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_users_list_"))
|
||
async def handle_admin_users_list(callback: types.CallbackQuery):
|
||
"""Отображение списка всех пользователей с пагинацией"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
# Извлекаем номер страницы
|
||
page = int(callback.data.replace("admin_users_list_", ""))
|
||
per_page = 10 # Пользователей на странице
|
||
|
||
# Получаем пользователей из БД
|
||
result = await db.get_all_users_paginated(page, per_page)
|
||
|
||
users = result.get('users', [])
|
||
total = result.get('total', 0)
|
||
total_pages = result.get('total_pages', 0)
|
||
current_page = result.get('current_page', 1)
|
||
|
||
if not users:
|
||
await callback.message.edit_text(
|
||
"Нажмите на пользователя для управления:",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад к пользователям",
|
||
callback_data="admin_users"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
await callback.answer()
|
||
return
|
||
|
||
# Формируем список пользователей
|
||
keyboard_buttons = []
|
||
|
||
from datetime import datetime, timezone
|
||
|
||
for user in users:
|
||
user_id = user['user_id']
|
||
username = user.get('username') or f"User {user_id}"
|
||
is_active = user.get('is_active', False)
|
||
created_at = user.get('created_at')
|
||
|
||
# Формируем статус активности
|
||
status_emoji = "✅" if is_active else "❌"
|
||
|
||
# Формируем иконку подписки (если активна)
|
||
sub_emoji = "💎" if is_active else ""
|
||
|
||
# Подарок для новых пользователей (созданы сегодня)
|
||
gift_emoji = ""
|
||
if created_at:
|
||
if hasattr(created_at, 'date'):
|
||
if created_at.date() == datetime.now().date():
|
||
gift_emoji = "🎁"
|
||
elif isinstance(created_at, str):
|
||
try:
|
||
created_date = datetime.fromisoformat(created_at)
|
||
if created_date.date() == datetime.now().date():
|
||
gift_emoji = "🎁"
|
||
except:
|
||
pass
|
||
|
||
# Вычисляем время с момента регистрации
|
||
time_ago = ""
|
||
if created_at:
|
||
if hasattr(created_at, 'replace'):
|
||
try:
|
||
now = datetime.now()
|
||
if hasattr(created_at, 'tzinfo') and created_at.tzinfo is not None:
|
||
now = now.replace(tzinfo=timezone.utc)
|
||
diff = now - created_at
|
||
seconds = int(diff.total_seconds())
|
||
|
||
if seconds < 60:
|
||
time_ago = f"{seconds} сек. назад"
|
||
elif seconds < 3600:
|
||
minutes = seconds // 60
|
||
time_ago = f"{minutes} мин. назад"
|
||
elif seconds < 86400:
|
||
hours = seconds // 3600
|
||
time_ago = f"{hours} ч. назад"
|
||
else:
|
||
days = seconds // 86400
|
||
time_ago = f"{days} дн. назад"
|
||
except:
|
||
time_ago = str(created_at)[:10]
|
||
else:
|
||
time_ago = str(created_at)[:10]
|
||
|
||
# Формируем текст для кнопки пользователя
|
||
user_button_text = f"{status_emoji} {gift_emoji}{sub_emoji} {username} | {time_ago}"
|
||
|
||
# Ограничиваем длину текста кнопки (максимум 64 символа)
|
||
if len(user_button_text) > 64:
|
||
user_button_text = user_button_text[:61] + "..."
|
||
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text=user_button_text,
|
||
callback_data=f"admin_user_view_{user_id}"
|
||
)
|
||
])
|
||
|
||
# Добавляем пагинацию
|
||
pagination_buttons = []
|
||
|
||
# Кнопка предыдущей страницы
|
||
if current_page > 1:
|
||
pagination_buttons.append(
|
||
InlineKeyboardButton(
|
||
text="⬅️",
|
||
callback_data=f"admin_users_list_{current_page - 1}"
|
||
)
|
||
)
|
||
|
||
# Номер текущей страницы
|
||
pagination_buttons.append(
|
||
InlineKeyboardButton(
|
||
text=f"{current_page}/{total_pages}",
|
||
callback_data="noop"
|
||
)
|
||
)
|
||
|
||
# Кнопка следующей страницы
|
||
if current_page < total_pages:
|
||
pagination_buttons.append(
|
||
InlineKeyboardButton(
|
||
text="➡️",
|
||
callback_data=f"admin_users_list_{current_page + 1}"
|
||
)
|
||
)
|
||
|
||
keyboard_buttons.append(pagination_buttons)
|
||
|
||
# Кнопка поиска
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text="🔍 Поиск",
|
||
callback_data="admin_user_search"
|
||
)
|
||
])
|
||
|
||
# Кнопка назад
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data="admin_users"
|
||
)
|
||
])
|
||
|
||
await callback.message.edit_text(
|
||
"Нажмите на пользователя для управления:",
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_sync_remnawave")
|
||
async def handle_admin_sync_remnawave(callback: types.CallbackQuery):
|
||
"""Синхронизация всех пользователей из Remnawave в БД"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
# Отправляем сообщение о начале синхронизации
|
||
progress_message = await callback.message.answer("🔄 Начинаю синхронизацию из Remnawave...\n\nЭто может занять несколько минут.")
|
||
|
||
# Запускаем синхронизацию (импорт из Remnawave в БД)
|
||
result = await remnawave.sync_all_users()
|
||
|
||
if result.get("success"):
|
||
result_text = (
|
||
"✅ Синхронизация завершена!\n\n"
|
||
f"📊 Результаты:\n"
|
||
f"• Всего пользователей в Remnawave: {result['total']}\n"
|
||
f"• Импортировано новых: {result['imported']}\n"
|
||
f"• Обновлено существующих: {result['updated']}\n"
|
||
f"• Ошибок: {result['errors']}"
|
||
)
|
||
|
||
if result.get("error_details"):
|
||
result_text += f"\n\n⚠️ Ошибки (первые 10):\n"
|
||
for error in result["error_details"]:
|
||
result_text += f"• {error}\n"
|
||
else:
|
||
result_text = f"❌ Ошибка синхронизации: {result.get('error', 'Неизвестная ошибка')}"
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад к пользователям",
|
||
callback_data="admin_users"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
try:
|
||
await progress_message.edit_text(
|
||
result_text,
|
||
reply_markup=keyboard
|
||
)
|
||
except:
|
||
await callback.message.answer(
|
||
result_text,
|
||
reply_markup=keyboard
|
||
)
|
||
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_user_view_"))
|
||
async def handle_admin_user_view(callback: types.CallbackQuery):
|
||
"""Просмотр информации о пользователе"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
user_id = int(callback.data.replace("admin_user_view_", ""))
|
||
|
||
# Получаем информацию о пользователе из БД
|
||
connection = db.get_connection()
|
||
cursor = connection.cursor(dictionary=True)
|
||
cursor.execute(
|
||
"SELECT * FROM subscriptions WHERE user_id = %s",
|
||
(user_id,)
|
||
)
|
||
user = cursor.fetchone()
|
||
cursor.close()
|
||
connection.close()
|
||
|
||
if not user:
|
||
await callback.answer("Пользователь не найден", show_alert=True)
|
||
return
|
||
|
||
# Формируем текст с информацией о пользователе
|
||
user_text = f"👤 Информация о пользователе\n\n"
|
||
user_text += f"ID: {user['user_id']}\n"
|
||
user_text += f"Username: @{user['username'] or 'Не указан'}\n"
|
||
user_text += f"Статус: {'✅ Активен' if user['is_active'] else '❌ Неактивен'}\n\n"
|
||
|
||
if user.get('subscription_start'):
|
||
user_text += f"Начало подписки: {user['subscription_start'].strftime('%d.%m.%Y %H:%M') if hasattr(user['subscription_start'], 'strftime') else user['subscription_start']}\n"
|
||
|
||
if user.get('subscription_end'):
|
||
user_text += f"Окончание подписки: {user['subscription_end'].strftime('%d.%m.%Y %H:%M') if hasattr(user['subscription_end'], 'strftime') else user['subscription_end']}\n"
|
||
|
||
user_text += f"\nДата регистрации: {user['created_at'].strftime('%d.%m.%Y %H:%M') if hasattr(user['created_at'], 'strftime') else user['created_at']}\n"
|
||
|
||
# Кнопки управления пользователем
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="✅ Активировать подписку" if not user['is_active'] else "❌ Деактивировать",
|
||
callback_data=f"admin_user_toggle_sub_{user_id}"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="📋 Тикеты пользователя",
|
||
callback_data=f"admin_user_tickets_{user_id}"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад к списку",
|
||
callback_data="admin_users_list_1"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
user_text,
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_user_search")
|
||
async def handle_admin_user_search(callback: types.CallbackQuery):
|
||
"""Заглушка для поиска пользователей"""
|
||
await callback.answer("Функция поиска скоро будет доступна", show_alert=True)
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "noop")
|
||
async def handle_noop(callback: types.CallbackQuery):
|
||
"""Заглушка для неактивных кнопок"""
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_user_toggle_sub_"))
|
||
async def handle_admin_user_toggle_sub(callback: types.CallbackQuery):
|
||
"""Переключение статуса подписки пользователя"""
|
||
user_id = int(callback.data.replace("admin_user_toggle_sub_", ""))
|
||
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
# Получаем текущего пользователя
|
||
connection = db.get_connection()
|
||
cursor = connection.cursor(dictionary=True)
|
||
cursor.execute("SELECT is_active FROM subscriptions WHERE user_id = %s", (user_id,))
|
||
user = cursor.fetchone()
|
||
|
||
if not user:
|
||
await callback.answer("Пользователь не найден", show_alert=True)
|
||
cursor.close()
|
||
connection.close()
|
||
return
|
||
|
||
# Переключаем статус
|
||
new_status = not user['is_active']
|
||
if new_status:
|
||
# Активируем подписку на 30 дней
|
||
cursor.execute(
|
||
"""UPDATE subscriptions
|
||
SET is_active = TRUE,
|
||
subscription_start = NOW(),
|
||
subscription_end = DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||
WHERE user_id = %s""",
|
||
(user_id,)
|
||
)
|
||
else:
|
||
# Деактивируем подписку
|
||
cursor.execute(
|
||
"UPDATE subscriptions SET is_active = FALSE WHERE user_id = %s",
|
||
(user_id,)
|
||
)
|
||
|
||
connection.commit()
|
||
cursor.close()
|
||
connection.close()
|
||
|
||
await callback.answer(f"Подписка {'активирована' if new_status else 'деактивирована'}")
|
||
|
||
# Обновляем экран
|
||
await handle_admin_user_view(callback)
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_user_tickets_"))
|
||
async def handle_admin_user_tickets(callback: types.CallbackQuery):
|
||
"""Просмотр тикетов пользователя"""
|
||
user_id = int(callback.data.replace("admin_user_tickets_", ""))
|
||
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
# Получаем тикеты пользователя
|
||
tickets = await db.get_user_tickets(user_id)
|
||
|
||
if not tickets:
|
||
await callback.message.answer(
|
||
"📋 Тикеты пользователя\n\n"
|
||
"У пользователя нет тикетов.",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад к пользователю",
|
||
callback_data=f"admin_user_view_{user_id}"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
await callback.answer()
|
||
return
|
||
|
||
# Формируем список тикетов
|
||
tickets_text = f"📋 Тикеты пользователя\n\n"
|
||
keyboard_buttons = []
|
||
|
||
for ticket in tickets[:10]:
|
||
status_emoji = "🟢" if ticket['status'] == 'open' else "🔵" if ticket['status'] == 'answered' else "⚫"
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text=f"{status_emoji} #{ticket['id']} - {ticket['status']}",
|
||
callback_data=f"admin_ticket_view_{ticket['id']}"
|
||
)
|
||
])
|
||
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад к пользователю",
|
||
callback_data=f"admin_user_view_{user_id}"
|
||
)
|
||
])
|
||
|
||
await callback.message.answer(
|
||
tickets_text,
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_all_tickets")
|
||
async def handle_admin_all_tickets(callback: types.CallbackQuery):
|
||
"""Все тикеты (последние 10)"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
# Получаем все тикеты (последние 10)
|
||
connection = db.get_connection()
|
||
cursor = connection.cursor(dictionary=True)
|
||
cursor.execute(
|
||
"""SELECT id, user_id, username, message, status, created_at
|
||
FROM tickets ORDER BY created_at DESC LIMIT 10"""
|
||
)
|
||
tickets = cursor.fetchall()
|
||
cursor.close()
|
||
connection.close()
|
||
|
||
if not tickets:
|
||
await callback.answer("Тикетов нет", show_alert=True)
|
||
return
|
||
|
||
tickets_text = "📋 Последние тикеты (10):\n\n"
|
||
keyboard_buttons = []
|
||
|
||
for ticket in tickets:
|
||
status_emoji = "🟢" if ticket['status'] == 'open' else "🔵" if ticket['status'] == 'answered' else "⚫"
|
||
created_at = ticket['created_at'].strftime("%d.%m.%Y %H:%M") if ticket['created_at'] else 'Неизвестно'
|
||
|
||
tickets_text += (
|
||
f"{status_emoji} #{ticket['id']} | @{ticket['username']} | {ticket['status']}\n"
|
||
)
|
||
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text=f"#{ticket['id']} - @{ticket['username']}",
|
||
callback_data=f"admin_ticket_view_{ticket['id']}"
|
||
)
|
||
])
|
||
|
||
keyboard_buttons.append([
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data="admin_back"
|
||
)
|
||
])
|
||
|
||
await callback.message.answer(
|
||
tickets_text,
|
||
reply_markup=InlineKeyboardMarkup(inline_keyboard=keyboard_buttons)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_ticket_view_"))
|
||
async def handle_admin_ticket_view(callback: types.CallbackQuery):
|
||
"""Просмотр тикета админом"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
ticket_id = int(callback.data.replace("admin_ticket_view_", ""))
|
||
ticket = await db.get_ticket(ticket_id)
|
||
|
||
if not ticket:
|
||
await callback.answer("Тикет не найден", show_alert=True)
|
||
return
|
||
|
||
status_text = {
|
||
'open': '🟢 Открыт',
|
||
'answered': '🔵 Дан ответ',
|
||
'closed': '⚫ Закрыт'
|
||
}.get(ticket['status'], ticket['status'])
|
||
|
||
ticket_text = f"📋 Тикет #{ticket['id']}\n\n"
|
||
ticket_text += f"Пользователь: @{ticket['username']} ({ticket['user_id']})\n"
|
||
ticket_text += f"Статус: {status_text}\n"
|
||
ticket_text += f"Дата создания: {ticket['created_at'].strftime('%d.%m.%Y %H:%M') if ticket['created_at'] else 'Неизвестно'}\n\n"
|
||
ticket_text += f"❓ Вопрос:\n{ticket['message']}\n\n"
|
||
|
||
if ticket['admin_response']:
|
||
ticket_text += f"💬 Ответ поддержки:\n{ticket['admin_response']}\n\n"
|
||
|
||
if ticket['status'] == 'closed':
|
||
closed_by = ticket.get('closed_by', 'Неизвестно')
|
||
closed_at = ticket.get('closed_at')
|
||
ticket_text += f"🔒 Закрыл: {closed_by}\n"
|
||
if closed_at:
|
||
ticket_text += f"🕒 Дата: {closed_at.strftime('%d.%m.%Y %H:%M') if hasattr(closed_at, 'strftime') else closed_at}\n"
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="✏️ Ответить",
|
||
callback_data=f"admin_reply_{ticket['id']}"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="✅ Закрыть",
|
||
callback_data=f"admin_close_{ticket['id']}"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data="admin_all_tickets"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.answer(ticket_text, reply_markup=keyboard)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_reply_"))
|
||
async def handle_admin_reply(callback: types.CallbackQuery):
|
||
"""Ответ на тикет"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
ticket_id = int(callback.data.replace("admin_reply_", ""))
|
||
|
||
# Устанавливаем режим ответа
|
||
ticket_append_mode[callback.from_user.id] = f"admin_reply_{ticket_id}"
|
||
|
||
await callback.message.answer(
|
||
f"✏️ Ответ на тикет #{ticket_id}\n\n"
|
||
f"Напишите ваш ответ одним сообщением:",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="❌ Отмена",
|
||
callback_data="admin_cancel_reply"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_cancel_reply")
|
||
async def handle_admin_cancel_reply(callback: types.CallbackQuery):
|
||
"""Отмена ответа админа"""
|
||
if callback.from_user.id in ticket_append_mode:
|
||
del ticket_append_mode[callback.from_user.id]
|
||
|
||
await callback.message.edit_text(
|
||
"❌ Отменено\n\nВыберите действие:",
|
||
reply_markup=InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад в админку",
|
||
callback_data="admin_back"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data.startswith("admin_close_"))
|
||
async def handle_admin_close(callback: types.CallbackQuery):
|
||
"""Закрытие тикета админом"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
ticket_id = int(callback.data.replace("admin_close_", ""))
|
||
admin_username = callback.from_user.username or callback.from_user.first_name
|
||
|
||
await db.close_ticket(ticket_id, f"admin:{admin_username}")
|
||
|
||
# Уведомляем пользователя
|
||
ticket = await db.get_ticket(ticket_id)
|
||
if ticket:
|
||
try:
|
||
await callback.bot.send_message(
|
||
chat_id=ticket['user_id'],
|
||
text=f"📋 Ваш тикет #{ticket_id} был закрыт администратором.\n\n"
|
||
f"Если у вас остались вопросы, создайте новый тикет."
|
||
)
|
||
except:
|
||
pass
|
||
|
||
await callback.answer("Тикет закрыт")
|
||
|
||
# Обновляем сообщение
|
||
try:
|
||
await callback.message.delete()
|
||
except:
|
||
pass
|
||
|
||
|
||
@admin_router.callback_query(lambda c: c.data == "admin_back")
|
||
async def handle_admin_back(callback: types.CallbackQuery):
|
||
"""Назад в админку"""
|
||
if not is_admin(callback.from_user.id):
|
||
await callback.answer("Доступ запрещён", show_alert=True)
|
||
return
|
||
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text="📋 Все тикеты",
|
||
callback_data="admin_all_tickets"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔍 Тикет по ID",
|
||
callback_data="admin_ticket_by_id"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="👥 Пользователи",
|
||
callback_data="admin_users"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="⚙️ Активировать подписку",
|
||
callback_data="admin_activate_sub"
|
||
)
|
||
],
|
||
[
|
||
InlineKeyboardButton(
|
||
text="🔙 Назад",
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
await callback.message.edit_text(
|
||
"⚙️ Админ-панель\n\nВыберите действие:",
|
||
reply_markup=keyboard
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("payment_approve_"))
|
||
async def handle_payment_approve(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Аппрув'"""
|
||
# payment_approve_{user_id}_{tariff}_{days}
|
||
data = callback.data.replace("payment_approve_", "").split("_")
|
||
|
||
# Получаем locale для клавиатур
|
||
locale = await get_user_locale(callback.from_user.id)
|
||
|
||
if len(data) >= 3:
|
||
user_id = int(data[0])
|
||
tariff = data[1]
|
||
days = int(data[2])
|
||
|
||
# Создаём подписку через Remnawave API
|
||
api_result = await remnawave.create_subscription(user_id, days, tariff)
|
||
|
||
if api_result.get("success"):
|
||
# Активируем подписку в локальной БД
|
||
await db.activate_subscription(user_id, days)
|
||
|
||
# Формируем сообщение для пользователя
|
||
subscription_url = api_result.get("subscription_url")
|
||
username = api_result.get("username", f"subs{user_id}")
|
||
|
||
print(f"🔗 subscription_url: {subscription_url}")
|
||
print(f"👤 username: {username}")
|
||
|
||
if subscription_url:
|
||
user_message = (
|
||
f"✅ Оплата подтверждена!\n\n"
|
||
f"Ваша подписка активирована на {days} дней.\n\n"
|
||
f"Ссылка на подключение : {subscription_url}\n\n"
|
||
f"Нажмите на ссылку выше, чтобы добавить VPN в приложение.\n\n"
|
||
f"Спасибо за покупку! 🎉"
|
||
)
|
||
else:
|
||
user_message = (
|
||
f"✅ Оплата подтверждена!\n\n"
|
||
f"Ваша подписка активирована на {days} дней.\n"
|
||
f"Username: <code>{username}</code>\n\n"
|
||
f"Ссылка на подключение будет отправлена администратором.\n\n"
|
||
f"Спасибо за покупку! 🎉"
|
||
)
|
||
|
||
# Клавиатура с кнопкой "В главное меню"
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
# Отправляем уведомление пользователю
|
||
try:
|
||
await callback.bot.send_message(
|
||
chat_id=user_id,
|
||
text=user_message,
|
||
reply_markup=keyboard
|
||
)
|
||
print(f"✅ Сообщение отправлено пользователю {user_id}")
|
||
except Exception as e:
|
||
print(f"❌ Не удалось отправить уведомление пользователю {user_id}: {e}")
|
||
else:
|
||
# Ошибка API
|
||
error_msg = f"⚠️ Подписка не создана\n\nОшибка: {api_result.get('error', 'Неизвестная ошибка')}\n\nОбратитесь в техподдержку."
|
||
|
||
# Клавиатура с кнопкой "В главное меню"
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
try:
|
||
await callback.bot.send_message(
|
||
chat_id=user_id,
|
||
text=error_msg,
|
||
reply_markup=keyboard
|
||
)
|
||
except:
|
||
pass
|
||
|
||
# Удаляем кнопки из сообщения в канале
|
||
try:
|
||
await callback.message.edit_reply_markup(reply_markup=None)
|
||
except:
|
||
pass
|
||
|
||
# Добавляем пометку в канал
|
||
try:
|
||
status_text = "✅ ОПЛАЧЕНО" if api_result.get("success") else "⚠️ ОШИБКА API"
|
||
await callback.message.edit_caption(
|
||
caption=f"{callback.message.caption}\n\n{status_text}",
|
||
reply_markup=None
|
||
)
|
||
except:
|
||
pass
|
||
|
||
await callback.answer("Готово!")
|
||
|
||
|
||
@main_router.callback_query(lambda c: c.data.startswith("payment_decline_"))
|
||
async def handle_payment_decline(callback: types.CallbackQuery):
|
||
"""Обработчик кнопки 'Отклонить'"""
|
||
# payment_decline_{user_id}_{tariff}
|
||
data = callback.data.replace("payment_decline_", "").split("_")
|
||
|
||
# Получаем locale для клавиатур
|
||
locale = await get_user_locale(callback.from_user.id)
|
||
|
||
if len(data) >= 2:
|
||
user_id = int(data[0])
|
||
|
||
# Клавиатура с кнопкой "В главное меню"
|
||
keyboard = InlineKeyboardMarkup(
|
||
inline_keyboard=[
|
||
[
|
||
InlineKeyboardButton(
|
||
text=localization.get("buttons.back", locale),
|
||
callback_data="back_to_main"
|
||
)
|
||
]
|
||
]
|
||
)
|
||
|
||
# Отправляем уведомление пользователю
|
||
try:
|
||
await callback.bot.send_message(
|
||
chat_id=user_id,
|
||
text=f"❌ Оплата отклонена.\n\nПо вопросам обращайтесь в техподдержку.",
|
||
reply_markup=keyboard
|
||
)
|
||
except Exception as e:
|
||
print(f"Не удалось отправить уведомление пользователю {user_id}: {e}")
|
||
|
||
# Удаляем кнопки из сообщения в канале
|
||
try:
|
||
await callback.message.edit_reply_markup(reply_markup=None)
|
||
except:
|
||
pass
|
||
|
||
# Добавляем пометку в канал
|
||
try:
|
||
await callback.message.edit_caption(
|
||
caption=f"{callback.message.caption}\n\n❌ ОТКЛОНЕНО",
|
||
reply_markup=None
|
||
)
|
||
except:
|
||
pass
|
||
|
||
await callback.answer("Оплата отклонена")
|