mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-04 03:00:26 +00:00
469 lines
20 KiB
Python
469 lines
20 KiB
Python
import logging
|
||
import re
|
||
import html
|
||
import contextlib
|
||
from aiogram import Dispatcher, types, F
|
||
from aiogram.fsm.context import FSMContext
|
||
from aiogram.fsm.state import State, StatesGroup
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database.models import User
|
||
from app.config import settings
|
||
from app.localization.texts import get_texts
|
||
from app.utils.decorators import admin_required, error_handler
|
||
from app.services.support_settings_service import SupportSettingsService
|
||
from app.states import SupportSettingsStates
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _get_support_settings_keyboard(language: str) -> types.InlineKeyboardMarkup:
|
||
texts = get_texts(language)
|
||
mode = SupportSettingsService.get_system_mode()
|
||
menu_enabled = SupportSettingsService.is_support_menu_enabled()
|
||
admin_notif = SupportSettingsService.get_admin_ticket_notifications_enabled()
|
||
user_notif = SupportSettingsService.get_user_ticket_notifications_enabled()
|
||
sla_enabled = SupportSettingsService.get_sla_enabled()
|
||
sla_minutes = SupportSettingsService.get_sla_minutes()
|
||
|
||
rows: list[list[types.InlineKeyboardButton]] = []
|
||
|
||
status_enabled = texts.t("ADMIN_SUPPORT_SETTINGS_STATUS_ENABLED", "Включены")
|
||
status_disabled = texts.t("ADMIN_SUPPORT_SETTINGS_STATUS_DISABLED", "Отключены")
|
||
|
||
def mode_button(label_key: str, default: str, active: bool) -> str:
|
||
prefix = "🔘" if active else "⚪"
|
||
return f"{prefix} {texts.t(label_key, default)}"
|
||
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=(
|
||
f"{'✅' if menu_enabled else '🚫'} "
|
||
f"{texts.t('ADMIN_SUPPORT_SETTINGS_MENU_LABEL', 'Пункт «Техподдержка» в меню')}"
|
||
),
|
||
callback_data="admin_support_toggle_menu"
|
||
)
|
||
])
|
||
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=mode_button("ADMIN_SUPPORT_SETTINGS_MODE_TICKETS", "Тикеты", mode == "tickets"),
|
||
callback_data="admin_support_mode_tickets"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text=mode_button("ADMIN_SUPPORT_SETTINGS_MODE_CONTACT", "Контакт", mode == "contact"),
|
||
callback_data="admin_support_mode_contact"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text=mode_button("ADMIN_SUPPORT_SETTINGS_MODE_BOTH", "Оба", mode == "both"),
|
||
callback_data="admin_support_mode_both"
|
||
),
|
||
])
|
||
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_SUPPORT_SETTINGS_EDIT_DESCRIPTION", "📝 Изменить описание"),
|
||
callback_data="admin_support_edit_desc"
|
||
)
|
||
])
|
||
|
||
# Notifications block
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=(
|
||
f"{'🔔' if admin_notif else '🔕'} "
|
||
f"{texts.t('ADMIN_SUPPORT_SETTINGS_ADMIN_NOTIFICATIONS', 'Админ-уведомления')}: "
|
||
f"{status_enabled if admin_notif else status_disabled}"
|
||
),
|
||
callback_data="admin_support_toggle_admin_notifications"
|
||
)
|
||
])
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=(
|
||
f"{'🔔' if user_notif else '🔕'} "
|
||
f"{texts.t('ADMIN_SUPPORT_SETTINGS_USER_NOTIFICATIONS', 'Пользовательские уведомления')}: "
|
||
f"{status_enabled if user_notif else status_disabled}"
|
||
),
|
||
callback_data="admin_support_toggle_user_notifications"
|
||
)
|
||
])
|
||
|
||
# SLA block
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=(
|
||
f"{'⏰' if sla_enabled else '⏹️'} "
|
||
f"{texts.t('ADMIN_SUPPORT_SETTINGS_SLA_LABEL', 'SLA')}: "
|
||
f"{status_enabled if sla_enabled else status_disabled}"
|
||
),
|
||
callback_data="admin_support_toggle_sla"
|
||
)
|
||
])
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_SUPPORT_SETTINGS_SLA_TIME", "⏳ Время SLA: {minutes} мин").format(minutes=sla_minutes),
|
||
callback_data="admin_support_set_sla_minutes"
|
||
)
|
||
])
|
||
|
||
# Moderators
|
||
moderators = SupportSettingsService.get_moderators()
|
||
mod_count = len(moderators)
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_SUPPORT_SETTINGS_MODERATORS_COUNT", "🧑⚖️ Модераторы: {count}").format(count=mod_count),
|
||
callback_data="admin_support_list_moderators"
|
||
)
|
||
])
|
||
rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_SUPPORT_SETTINGS_ADD_MODERATOR", "➕ Назначить модератора"),
|
||
callback_data="admin_support_add_moderator"
|
||
),
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_SUPPORT_SETTINGS_REMOVE_MODERATOR", "➖ Удалить модератора"),
|
||
callback_data="admin_support_remove_moderator"
|
||
)
|
||
])
|
||
|
||
rows.append([
|
||
types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_support")
|
||
])
|
||
|
||
return types.InlineKeyboardMarkup(inline_keyboard=rows)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def show_support_settings(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
texts = get_texts(db_user.language)
|
||
desc = SupportSettingsService.get_support_info_text(db_user.language)
|
||
await callback.message.edit_text(
|
||
texts.t("ADMIN_SUPPORT_SETTINGS_TITLE", "🛟 <b>Настройки поддержки</b>") + "\n\n" +
|
||
texts.t("ADMIN_SUPPORT_SETTINGS_DESCRIPTION", "Режим работы и видимость в меню. Ниже текущее описание меню поддержки:") + "\n\n" +
|
||
desc,
|
||
reply_markup=_get_support_settings_keyboard(db_user.language),
|
||
parse_mode="HTML"
|
||
)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_support_menu(
|
||
callback: types.CallbackQuery,
|
||
db_user: User,
|
||
db: AsyncSession
|
||
):
|
||
current = SupportSettingsService.is_support_menu_enabled()
|
||
SupportSettingsService.set_support_menu_enabled(not current)
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_admin_notifications(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
current = SupportSettingsService.get_admin_ticket_notifications_enabled()
|
||
SupportSettingsService.set_admin_ticket_notifications_enabled(not current)
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_user_notifications(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
current = SupportSettingsService.get_user_ticket_notifications_enabled()
|
||
SupportSettingsService.set_user_ticket_notifications_enabled(not current)
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def toggle_sla(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
current = SupportSettingsService.get_sla_enabled()
|
||
SupportSettingsService.set_sla_enabled(not current)
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
class SupportAdvancedStates(StatesGroup):
|
||
waiting_for_sla_minutes = State()
|
||
waiting_for_moderator_id = State()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_set_sla_minutes(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
await callback.message.edit_text(
|
||
texts.t(
|
||
"ADMIN_SUPPORT_SLA_SETUP_PROMPT",
|
||
"⏳ <b>Настройка SLA</b>\n\nВведите количество минут ожидания ответа (целое число > 0):"
|
||
),
|
||
parse_mode="HTML",
|
||
reply_markup=types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]]
|
||
)
|
||
)
|
||
await state.set_state(SupportAdvancedStates.waiting_for_sla_minutes)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def handle_sla_minutes(message: types.Message, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
text = (message.text or "").strip()
|
||
try:
|
||
minutes = int(text)
|
||
if minutes <= 0 or minutes > 1440:
|
||
raise ValueError()
|
||
except Exception:
|
||
await message.answer(texts.t("ADMIN_SUPPORT_SLA_INVALID", "❌ Введите корректное число минут (1-1440)"))
|
||
return
|
||
SupportSettingsService.set_sla_minutes(minutes)
|
||
await state.clear()
|
||
markup = types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]]
|
||
)
|
||
await message.answer(texts.t("ADMIN_SUPPORT_SLA_SAVED", "✅ Значение SLA сохранено"), reply_markup=markup)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_add_moderator(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
await callback.message.edit_text(
|
||
texts.t(
|
||
"ADMIN_SUPPORT_ASSIGN_MODERATOR_PROMPT",
|
||
"🧑⚖️ <b>Назначение модератора</b>\n\nОтправьте Telegram ID пользователя (число)"
|
||
),
|
||
parse_mode="HTML",
|
||
reply_markup=types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]]
|
||
)
|
||
)
|
||
await state.set_state(SupportAdvancedStates.waiting_for_moderator_id)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_remove_moderator(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
await callback.message.edit_text(
|
||
texts.t(
|
||
"ADMIN_SUPPORT_REMOVE_MODERATOR_PROMPT",
|
||
"🧑⚖️ <b>Удаление модератора</b>\n\nОтправьте Telegram ID пользователя (число)"
|
||
),
|
||
parse_mode="HTML",
|
||
reply_markup=types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]]
|
||
)
|
||
)
|
||
await state.set_state(SupportAdvancedStates.waiting_for_moderator_id)
|
||
# We'll reuse the same state; next message will decide action via flag
|
||
await state.update_data(action="remove_moderator")
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def handle_moderator_id(message: types.Message, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
data = await state.get_data()
|
||
action = data.get("action", "add")
|
||
text = (message.text or "").strip()
|
||
try:
|
||
tid = int(text)
|
||
except Exception:
|
||
await message.answer(texts.t("ADMIN_SUPPORT_INVALID_TELEGRAM_ID", "❌ Введите корректный Telegram ID (число)"))
|
||
return
|
||
if action == "remove_moderator":
|
||
ok = SupportSettingsService.remove_moderator(tid)
|
||
msg = (
|
||
texts.t("ADMIN_SUPPORT_MODERATOR_REMOVED_SUCCESS", "✅ Модератор {tid} удалён").format(tid=tid)
|
||
if ok
|
||
else texts.t("ADMIN_SUPPORT_MODERATOR_REMOVED_FAIL", "❌ Не удалось удалить модератора")
|
||
)
|
||
else:
|
||
ok = SupportSettingsService.add_moderator(tid)
|
||
msg = (
|
||
texts.t("ADMIN_SUPPORT_MODERATOR_ADDED_SUCCESS", "✅ Пользователь {tid} назначен модератором").format(tid=tid)
|
||
if ok
|
||
else texts.t("ADMIN_SUPPORT_MODERATOR_ADDED_FAIL", "❌ Не удалось назначить модератора")
|
||
)
|
||
await state.clear()
|
||
markup = types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]]
|
||
)
|
||
await message.answer(msg, reply_markup=markup)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def list_moderators(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
texts = get_texts(db_user.language)
|
||
moderators = SupportSettingsService.get_moderators()
|
||
if not moderators:
|
||
await callback.answer(texts.t("ADMIN_SUPPORT_MODERATORS_EMPTY", "Список пуст"), show_alert=True)
|
||
return
|
||
text = (
|
||
texts.t("ADMIN_SUPPORT_MODERATORS_TITLE", "🧑⚖️ <b>Модераторы</b>") +
|
||
"\n\n" + "\n".join([f"• <code>{tid}</code>" for tid in moderators])
|
||
)
|
||
markup = types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")]]
|
||
)
|
||
await callback.message.edit_text(text, parse_mode="HTML", reply_markup=markup)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def set_mode_tickets(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
SupportSettingsService.set_system_mode("tickets")
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def set_mode_contact(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
SupportSettingsService.set_system_mode("contact")
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def set_mode_both(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
SupportSettingsService.set_system_mode("both")
|
||
await show_support_settings(callback, db_user, db)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def start_edit_desc(callback: types.CallbackQuery, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
current_desc_html = SupportSettingsService.get_support_info_text(db_user.language)
|
||
# plain text for display-only code block
|
||
current_desc_plain = re.sub(r"<[^>]+>", "", current_desc_html)
|
||
|
||
kb_rows: list[list[types.InlineKeyboardButton]] = []
|
||
kb_rows.append([
|
||
types.InlineKeyboardButton(
|
||
text=texts.t("ADMIN_SUPPORT_SEND_DESCRIPTION", "📨 Прислать текст"),
|
||
callback_data="admin_support_send_desc"
|
||
)
|
||
])
|
||
# Подготовим блок контакта (отдельным инлайном)
|
||
from app.config import settings
|
||
support_contact_display = settings.get_support_contact_display()
|
||
kb_rows.append([
|
||
types.InlineKeyboardButton(text=texts.BACK, callback_data="admin_support_settings")
|
||
])
|
||
|
||
text_parts = [
|
||
texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_TITLE", "📝 <b>Редактирование описания поддержки</b>"),
|
||
"",
|
||
texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_CURRENT", "Текущее описание:"),
|
||
"",
|
||
f"<code>{html.escape(current_desc_plain)}</code>",
|
||
]
|
||
if support_contact_display:
|
||
text_parts += [
|
||
"",
|
||
texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_CONTACT_TITLE", "<b>Контакт для режима «Контакт»</b>"),
|
||
f"<code>{html.escape(support_contact_display)}</code>",
|
||
"",
|
||
texts.t("ADMIN_SUPPORT_EDIT_DESCRIPTION_CONTACT_HINT", "Добавьте в описание при необходимости."),
|
||
]
|
||
await callback.message.edit_text(
|
||
"\n".join(text_parts),
|
||
reply_markup=types.InlineKeyboardMarkup(inline_keyboard=kb_rows),
|
||
parse_mode="HTML"
|
||
)
|
||
await state.set_state(SupportSettingsStates.waiting_for_desc)
|
||
await callback.answer()
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def handle_new_desc(message: types.Message, db_user: User, db: AsyncSession, state: FSMContext):
|
||
texts = get_texts(db_user.language)
|
||
new_text = message.html_text or message.text
|
||
SupportSettingsService.set_support_info_text(db_user.language, new_text)
|
||
await state.clear()
|
||
markup = types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]]
|
||
)
|
||
await message.answer(texts.t("ADMIN_SUPPORT_DESCRIPTION_UPDATED", "✅ Описание обновлено."), reply_markup=markup)
|
||
|
||
|
||
@admin_required
|
||
@error_handler
|
||
async def send_desc_copy(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
# send plain text for easy copying
|
||
texts = get_texts(db_user.language)
|
||
current_desc_html = SupportSettingsService.get_support_info_text(db_user.language)
|
||
current_desc_plain = re.sub(r"<[^>]+>", "", current_desc_html)
|
||
# attach delete button to the sent message
|
||
markup = types.InlineKeyboardMarkup(
|
||
inline_keyboard=[[types.InlineKeyboardButton(text=texts.t("DELETE_MESSAGE", "🗑 Удалить"), callback_data="admin_support_delete_msg")]]
|
||
)
|
||
if len(current_desc_plain) <= 4000:
|
||
await callback.message.answer(current_desc_plain, reply_markup=markup)
|
||
else:
|
||
# split long messages (attach delete only to the last chunk)
|
||
chunk = 0
|
||
while chunk < len(current_desc_plain):
|
||
next_chunk = current_desc_plain[chunk:chunk+4000]
|
||
is_last = (chunk + 4000) >= len(current_desc_plain)
|
||
await callback.message.answer(next_chunk, reply_markup=(markup if is_last else None))
|
||
chunk += 4000
|
||
await callback.answer(texts.t("ADMIN_SUPPORT_DESCRIPTION_SENT", "Текст отправлен ниже"))
|
||
|
||
|
||
@error_handler
|
||
async def delete_sent_message(callback: types.CallbackQuery, db_user: User, db: AsyncSession):
|
||
# Allow admins and moderators to delete informational notifications
|
||
try:
|
||
may_delete = (settings.is_admin(callback.from_user.id) or SupportSettingsService.is_moderator(callback.from_user.id))
|
||
except Exception:
|
||
may_delete = False
|
||
texts = get_texts(db_user.language if db_user else 'ru')
|
||
if not may_delete:
|
||
await callback.answer(texts.ACCESS_DENIED, show_alert=True)
|
||
return
|
||
try:
|
||
await callback.message.delete()
|
||
finally:
|
||
with contextlib.suppress(Exception):
|
||
await callback.answer(texts.t("ADMIN_SUPPORT_MESSAGE_DELETED", "Сообщение удалено"))
|
||
|
||
|
||
def register_handlers(dp: Dispatcher):
|
||
dp.callback_query.register(show_support_settings, F.data == "admin_support_settings")
|
||
dp.callback_query.register(toggle_support_menu, F.data == "admin_support_toggle_menu")
|
||
dp.callback_query.register(set_mode_tickets, F.data == "admin_support_mode_tickets")
|
||
dp.callback_query.register(set_mode_contact, F.data == "admin_support_mode_contact")
|
||
dp.callback_query.register(set_mode_both, F.data == "admin_support_mode_both")
|
||
dp.callback_query.register(start_edit_desc, F.data == "admin_support_edit_desc")
|
||
dp.callback_query.register(send_desc_copy, F.data == "admin_support_send_desc")
|
||
dp.callback_query.register(delete_sent_message, F.data == "admin_support_delete_msg")
|
||
dp.callback_query.register(toggle_admin_notifications, F.data == "admin_support_toggle_admin_notifications")
|
||
dp.callback_query.register(toggle_user_notifications, F.data == "admin_support_toggle_user_notifications")
|
||
dp.callback_query.register(toggle_sla, F.data == "admin_support_toggle_sla")
|
||
dp.callback_query.register(start_set_sla_minutes, F.data == "admin_support_set_sla_minutes")
|
||
dp.callback_query.register(start_add_moderator, F.data == "admin_support_add_moderator")
|
||
dp.callback_query.register(start_remove_moderator, F.data == "admin_support_remove_moderator")
|
||
dp.callback_query.register(list_moderators, F.data == "admin_support_list_moderators")
|
||
dp.message.register(handle_new_desc, SupportSettingsStates.waiting_for_desc)
|
||
dp.message.register(handle_sla_minutes, SupportAdvancedStates.waiting_for_sla_minutes)
|
||
dp.message.register(handle_moderator_id, SupportAdvancedStates.waiting_for_moderator_id)
|
||
|
||
|