mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-03-02 08:11:32 +00:00
Merge pull request #431 from Fr1ngg/revert-430-bedolaga/fix-bot-settings-editing-issues
Revert "Fix bot config callbacks and add selectable options"
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import math
|
||||
import time
|
||||
from typing import Iterable, List, Optional, Tuple
|
||||
from typing import Iterable, List, Tuple
|
||||
|
||||
from aiogram import Dispatcher, F, types
|
||||
from aiogram.filters import BaseFilter, StateFilter
|
||||
@@ -142,17 +142,6 @@ def _parse_group_payload(payload: str) -> Tuple[str, int]:
|
||||
return group_key, page
|
||||
|
||||
|
||||
async def _resolve_key_from_token(
|
||||
callback: types.CallbackQuery, token: str
|
||||
) -> Optional[str]:
|
||||
key = bot_configuration_service.resolve_key_from_token(token)
|
||||
if key:
|
||||
return key
|
||||
|
||||
await callback.answer("Эта настройка больше недоступна", show_alert=True)
|
||||
return None
|
||||
|
||||
|
||||
def _get_grouped_categories() -> List[Tuple[str, str, List[Tuple[str, str, int]]]]:
|
||||
categories = bot_configuration_service.get_categories()
|
||||
categories_map = {key: (label, count) for key, label, count in categories}
|
||||
@@ -303,13 +292,12 @@ def _build_settings_keyboard(
|
||||
button_text = f"{definition.display_name} · {value_preview}"
|
||||
if len(button_text) > 64:
|
||||
button_text = button_text[:63] + "…"
|
||||
token = bot_configuration_service.get_callback_token(definition.key)
|
||||
rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=(
|
||||
f"botcfg_setting:{group_key}:{category_page}:{page}:{token}"
|
||||
f"botcfg_setting:{group_key}:{category_page}:{page}:{definition.key}"
|
||||
),
|
||||
)
|
||||
]
|
||||
@@ -360,24 +348,13 @@ def _build_setting_keyboard(
|
||||
) -> types.InlineKeyboardMarkup:
|
||||
definition = bot_configuration_service.get_definition(key)
|
||||
rows: list[list[types.InlineKeyboardButton]] = []
|
||||
token = bot_configuration_service.get_callback_token(key)
|
||||
|
||||
if definition.python_type is bool:
|
||||
rows.append([
|
||||
types.InlineKeyboardButton(
|
||||
text="🔁 Переключить",
|
||||
callback_data=(
|
||||
f"botcfg_toggle:{group_key}:{category_page}:{settings_page}:{token}"
|
||||
),
|
||||
)
|
||||
])
|
||||
|
||||
if definition.has_choices:
|
||||
rows.append([
|
||||
types.InlineKeyboardButton(
|
||||
text="📋 Выбрать",
|
||||
callback_data=(
|
||||
f"botcfg_choices:{group_key}:{category_page}:{settings_page}:{token}"
|
||||
f"botcfg_toggle:{group_key}:{category_page}:{settings_page}:{key}"
|
||||
),
|
||||
)
|
||||
])
|
||||
@@ -386,7 +363,7 @@ def _build_setting_keyboard(
|
||||
types.InlineKeyboardButton(
|
||||
text="✏️ Изменить",
|
||||
callback_data=(
|
||||
f"botcfg_edit:{group_key}:{category_page}:{settings_page}:{token}"
|
||||
f"botcfg_edit:{group_key}:{category_page}:{settings_page}:{key}"
|
||||
),
|
||||
)
|
||||
])
|
||||
@@ -396,7 +373,7 @@ def _build_setting_keyboard(
|
||||
types.InlineKeyboardButton(
|
||||
text="♻️ Сбросить",
|
||||
callback_data=(
|
||||
f"botcfg_reset:{group_key}:{category_page}:{settings_page}:{token}"
|
||||
f"botcfg_reset:{group_key}:{category_page}:{settings_page}:{key}"
|
||||
),
|
||||
)
|
||||
])
|
||||
@@ -413,49 +390,6 @@ def _build_setting_keyboard(
|
||||
return types.InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
|
||||
def _build_choices_keyboard(
|
||||
key: str,
|
||||
group_key: str,
|
||||
category_page: int,
|
||||
settings_page: int,
|
||||
) -> types.InlineKeyboardMarkup:
|
||||
token = bot_configuration_service.get_callback_token(key)
|
||||
choices = bot_configuration_service.get_choices(key)
|
||||
current_value = bot_configuration_service.get_current_value(key)
|
||||
|
||||
rows: list[list[types.InlineKeyboardButton]] = []
|
||||
|
||||
for index, (value, label) in enumerate(choices, start=1):
|
||||
if isinstance(value, str) and isinstance(current_value, str):
|
||||
is_selected = value.lower() == current_value.lower()
|
||||
else:
|
||||
is_selected = value == current_value
|
||||
prefix = "✅ " if is_selected else ""
|
||||
rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text=f"{prefix}{label}",
|
||||
callback_data=(
|
||||
f"botcfg_choice_set:{group_key}:{category_page}:{settings_page}:{token}:{index}"
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
rows.append(
|
||||
[
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=(
|
||||
f"botcfg_setting:{group_key}:{category_page}:{settings_page}:{token}"
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return types.InlineKeyboardMarkup(inline_keyboard=rows)
|
||||
|
||||
|
||||
def _render_setting_text(key: str) -> str:
|
||||
summary = bot_configuration_service.get_setting_summary(key)
|
||||
|
||||
@@ -561,10 +495,7 @@ async def show_bot_config_setting(
|
||||
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
|
||||
except ValueError:
|
||||
settings_page = 1
|
||||
token = parts[4] if len(parts) > 4 else ""
|
||||
key = await _resolve_key_from_token(callback, token)
|
||||
if not key:
|
||||
return
|
||||
key = parts[4] if len(parts) > 4 else ""
|
||||
text = _render_setting_text(key)
|
||||
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
@@ -596,10 +527,7 @@ async def start_edit_setting(
|
||||
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
|
||||
except ValueError:
|
||||
settings_page = 1
|
||||
token = parts[4] if len(parts) > 4 else ""
|
||||
key = await _resolve_key_from_token(callback, token)
|
||||
if not key:
|
||||
return
|
||||
key = parts[4] if len(parts) > 4 else ""
|
||||
definition = bot_configuration_service.get_definition(key)
|
||||
|
||||
summary = bot_configuration_service.get_setting_summary(key)
|
||||
@@ -627,7 +555,7 @@ async def start_edit_setting(
|
||||
types.InlineKeyboardButton(
|
||||
text=texts.BACK,
|
||||
callback_data=(
|
||||
f"botcfg_setting:{group_key}:{category_page}:{settings_page}:{token}"
|
||||
f"botcfg_setting:{group_key}:{category_page}:{settings_page}:{key}"
|
||||
),
|
||||
)
|
||||
]
|
||||
@@ -748,10 +676,7 @@ async def reset_setting(
|
||||
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
|
||||
except ValueError:
|
||||
settings_page = 1
|
||||
token = parts[4] if len(parts) > 4 else ""
|
||||
key = await _resolve_key_from_token(callback, token)
|
||||
if not key:
|
||||
return
|
||||
key = parts[4] if len(parts) > 4 else ""
|
||||
await bot_configuration_service.reset_value(db, key)
|
||||
await db.commit()
|
||||
|
||||
@@ -786,10 +711,7 @@ async def toggle_setting(
|
||||
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
|
||||
except ValueError:
|
||||
settings_page = 1
|
||||
token = parts[4] if len(parts) > 4 else ""
|
||||
key = await _resolve_key_from_token(callback, token)
|
||||
if not key:
|
||||
return
|
||||
key = parts[4] if len(parts) > 4 else ""
|
||||
current = bot_configuration_service.get_current_value(key)
|
||||
new_value = not bool(current)
|
||||
await bot_configuration_service.set_value(db, key, new_value)
|
||||
@@ -808,115 +730,6 @@ async def toggle_setting(
|
||||
await callback.answer("Обновлено")
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def show_setting_choices(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
parts = callback.data.split(":", 5)
|
||||
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
|
||||
try:
|
||||
category_page = max(1, int(parts[2])) if len(parts) > 2 else 1
|
||||
except ValueError:
|
||||
category_page = 1
|
||||
try:
|
||||
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
|
||||
except ValueError:
|
||||
settings_page = 1
|
||||
token = parts[4] if len(parts) > 4 else ""
|
||||
key = await _resolve_key_from_token(callback, token)
|
||||
if not key:
|
||||
return
|
||||
|
||||
choices = bot_configuration_service.get_choices(key)
|
||||
if not choices:
|
||||
await callback.answer("Для этой настройки недоступен выбор", show_alert=True)
|
||||
return
|
||||
|
||||
summary = bot_configuration_service.get_setting_summary(key)
|
||||
keyboard = _build_choices_keyboard(key, group_key, category_page, settings_page)
|
||||
|
||||
text = "\n".join(
|
||||
[
|
||||
"📋 <b>Выбор значения</b>",
|
||||
f"Название: {summary['name']}",
|
||||
f"Текущий вариант: {summary['current']}",
|
||||
"",
|
||||
"Выберите значение из списка ниже.",
|
||||
]
|
||||
)
|
||||
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
await _store_setting_context(
|
||||
state,
|
||||
key=key,
|
||||
group_key=group_key,
|
||||
category_page=category_page,
|
||||
settings_page=settings_page,
|
||||
)
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@admin_required
|
||||
@error_handler
|
||||
async def apply_setting_choice(
|
||||
callback: types.CallbackQuery,
|
||||
db_user: User,
|
||||
db: AsyncSession,
|
||||
state: FSMContext,
|
||||
):
|
||||
parts = callback.data.split(":", 6)
|
||||
group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY
|
||||
try:
|
||||
category_page = max(1, int(parts[2])) if len(parts) > 2 else 1
|
||||
except ValueError:
|
||||
category_page = 1
|
||||
try:
|
||||
settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1
|
||||
except ValueError:
|
||||
settings_page = 1
|
||||
token = parts[4] if len(parts) > 4 else ""
|
||||
key = await _resolve_key_from_token(callback, token)
|
||||
if not key:
|
||||
return
|
||||
|
||||
choices = bot_configuration_service.get_choices(key)
|
||||
if not choices:
|
||||
await callback.answer("Варианты для этой настройки недоступны", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
index = max(1, int(parts[5])) if len(parts) > 5 else 1
|
||||
except ValueError:
|
||||
await callback.answer("Некорректный вариант", show_alert=True)
|
||||
return
|
||||
|
||||
try:
|
||||
selected_value, label = choices[index - 1]
|
||||
except IndexError:
|
||||
await callback.answer("Некорректный вариант", show_alert=True)
|
||||
return
|
||||
|
||||
cast_value = bot_configuration_service.cast_choice_value(key, selected_value)
|
||||
await bot_configuration_service.set_value(db, key, cast_value)
|
||||
await db.commit()
|
||||
|
||||
text = _render_setting_text(key)
|
||||
keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page)
|
||||
await callback.message.edit_text(text, reply_markup=keyboard)
|
||||
await _store_setting_context(
|
||||
state,
|
||||
key=key,
|
||||
group_key=group_key,
|
||||
category_page=category_page,
|
||||
settings_page=settings_page,
|
||||
)
|
||||
await callback.answer(f"Установлено: {label}")
|
||||
|
||||
|
||||
def register_handlers(dp: Dispatcher) -> None:
|
||||
dp.callback_query.register(
|
||||
show_bot_config_menu,
|
||||
@@ -934,14 +747,6 @@ def register_handlers(dp: Dispatcher) -> None:
|
||||
show_bot_config_setting,
|
||||
F.data.startswith("botcfg_setting:"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
show_setting_choices,
|
||||
F.data.startswith("botcfg_choices:"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
apply_setting_choice,
|
||||
F.data.startswith("botcfg_choice_set:"),
|
||||
)
|
||||
dp.callback_query.register(
|
||||
start_edit_setting,
|
||||
F.data.startswith("botcfg_edit:"),
|
||||
|
||||
@@ -40,16 +40,11 @@ class SettingDefinition:
|
||||
python_type: Type[Any]
|
||||
type_label: str
|
||||
is_optional: bool
|
||||
choices: Optional[List[Tuple[Any, str]]] = None
|
||||
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
return _title_from_key(self.key)
|
||||
|
||||
@property
|
||||
def has_choices(self) -> bool:
|
||||
return bool(self.choices)
|
||||
|
||||
|
||||
class BotConfigurationService:
|
||||
EXCLUDED_KEYS: set[str] = {"BOT_TOKEN", "ADMIN_IDS"}
|
||||
@@ -88,55 +83,9 @@ class BotConfigurationService:
|
||||
"TELEGRAM": "Telegram Stars",
|
||||
}
|
||||
|
||||
CHOICES: Dict[str, List[Tuple[Any, str]]] = {
|
||||
"SUPPORT_SYSTEM_MODE": [
|
||||
("tickets", "Только тикеты"),
|
||||
("contact", "Только контакт"),
|
||||
("both", "Тикеты и контакт"),
|
||||
],
|
||||
"REMNAWAVE_AUTH_TYPE": [
|
||||
("api_key", "API Key"),
|
||||
("basic_auth", "Basic Auth"),
|
||||
],
|
||||
"REMNAWAVE_USER_DELETE_MODE": [
|
||||
("delete", "Удалять пользователя"),
|
||||
("disable", "Деактивировать пользователя"),
|
||||
],
|
||||
"DATABASE_MODE": [
|
||||
("auto", "Определять автоматически"),
|
||||
("postgresql", "PostgreSQL"),
|
||||
("sqlite", "SQLite"),
|
||||
],
|
||||
"TRAFFIC_SELECTION_MODE": [
|
||||
("selectable", "Пользователь выбирает пакет"),
|
||||
("fixed", "Фиксированный лимит"),
|
||||
],
|
||||
"DEFAULT_TRAFFIC_RESET_STRATEGY": [
|
||||
("NO_RESET", "Без сброса"),
|
||||
("DAY", "Ежедневно"),
|
||||
("WEEK", "Еженедельно"),
|
||||
("MONTH", "Ежемесячно"),
|
||||
],
|
||||
"CONNECT_BUTTON_MODE": [
|
||||
("guide", "Открывать гайд"),
|
||||
("miniapp_subscription", "Мини-приложение с подпиской"),
|
||||
("miniapp_custom", "Мини-приложение с кастомной ссылкой"),
|
||||
("link", "Прямая ссылка"),
|
||||
("happ_cryptolink", "Happ CryptoLink"),
|
||||
],
|
||||
"SERVER_STATUS_MODE": [
|
||||
("disabled", "Отключено"),
|
||||
("external_link", "Внешняя ссылка"),
|
||||
("external_link_miniapp", "Мини-приложение со ссылкой"),
|
||||
("xray", "Интеграция XrayChecker"),
|
||||
],
|
||||
}
|
||||
|
||||
_definitions: Dict[str, SettingDefinition] = {}
|
||||
_original_values: Dict[str, Any] = settings.model_dump()
|
||||
_overrides_raw: Dict[str, Optional[str]] = {}
|
||||
_callback_tokens: Dict[str, str] = {}
|
||||
_token_lookup: Dict[str, str] = {}
|
||||
|
||||
@classmethod
|
||||
def initialize_definitions(cls) -> None:
|
||||
@@ -157,8 +106,6 @@ class BotConfigurationService:
|
||||
category_key.capitalize() if category_key else "Прочее",
|
||||
)
|
||||
|
||||
choices = cls.CHOICES.get(key)
|
||||
|
||||
cls._definitions[key] = SettingDefinition(
|
||||
key=key,
|
||||
category_key=category_key or "other",
|
||||
@@ -166,7 +113,6 @@ class BotConfigurationService:
|
||||
python_type=python_type,
|
||||
type_label=type_label,
|
||||
is_optional=is_optional,
|
||||
choices=choices,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -271,7 +217,7 @@ class BotConfigurationService:
|
||||
@classmethod
|
||||
def format_value_for_list(cls, key: str) -> str:
|
||||
value = cls.get_current_value(key)
|
||||
formatted = cls._format_value_with_choices(key, value)
|
||||
formatted = cls.format_value(value)
|
||||
if formatted == "—":
|
||||
return formatted
|
||||
return _truncate(formatted)
|
||||
@@ -304,55 +250,6 @@ class BotConfigurationService:
|
||||
cls._overrides_raw.clear()
|
||||
await cls.initialize()
|
||||
|
||||
@classmethod
|
||||
def get_callback_token(cls, key: str) -> str:
|
||||
cls.initialize_definitions()
|
||||
if key in cls._callback_tokens:
|
||||
return cls._callback_tokens[key]
|
||||
|
||||
token = format(len(cls._callback_tokens) + 1, "x")
|
||||
while token in cls._token_lookup:
|
||||
token = format(len(cls._callback_tokens) + len(cls._token_lookup) + 1, "x")
|
||||
|
||||
cls._callback_tokens[key] = token
|
||||
cls._token_lookup[token] = key
|
||||
return token
|
||||
|
||||
@classmethod
|
||||
def resolve_key_from_token(cls, token: str) -> Optional[str]:
|
||||
if not token:
|
||||
return None
|
||||
return cls._token_lookup.get(token)
|
||||
|
||||
@classmethod
|
||||
def get_choices(cls, key: str) -> List[Tuple[Any, str]]:
|
||||
definition = cls.get_definition(key)
|
||||
return definition.choices or []
|
||||
|
||||
@classmethod
|
||||
def format_choice_label(cls, key: str, value: Any) -> Optional[str]:
|
||||
definition = cls.get_definition(key)
|
||||
if not definition.choices:
|
||||
return None
|
||||
|
||||
for stored_value, label in definition.choices:
|
||||
if cls._values_equal(stored_value, value):
|
||||
return f"{label} ({stored_value})"
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def cast_choice_value(cls, key: str, raw_value: Any) -> Any:
|
||||
definition = cls.get_definition(key)
|
||||
python_type = definition.python_type
|
||||
|
||||
if python_type is bool:
|
||||
return bool(raw_value)
|
||||
if python_type is int:
|
||||
return int(raw_value)
|
||||
if python_type is float:
|
||||
return float(raw_value)
|
||||
return str(raw_value)
|
||||
|
||||
@classmethod
|
||||
def deserialize_value(cls, key: str, raw_value: Optional[str]) -> Any:
|
||||
if raw_value is None:
|
||||
@@ -451,30 +348,14 @@ class BotConfigurationService:
|
||||
return {
|
||||
"key": key,
|
||||
"name": definition.display_name,
|
||||
"current": cls._format_value_with_choices(key, current),
|
||||
"original": cls._format_value_with_choices(key, original),
|
||||
"current": cls.format_value(current),
|
||||
"original": cls.format_value(original),
|
||||
"type": definition.type_label,
|
||||
"category_key": definition.category_key,
|
||||
"category_label": definition.category_label,
|
||||
"has_override": has_override,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _format_value_with_choices(cls, key: str, value: Any) -> str:
|
||||
formatted = cls.format_value(value)
|
||||
definition = cls.get_definition(key)
|
||||
if not definition.choices:
|
||||
return formatted
|
||||
|
||||
choice_label = cls.format_choice_label(key, value)
|
||||
return choice_label or formatted
|
||||
|
||||
@staticmethod
|
||||
def _values_equal(a: Any, b: Any) -> bool:
|
||||
if isinstance(a, str) and isinstance(b, str):
|
||||
return a.lower() == b.lower()
|
||||
return a == b
|
||||
|
||||
|
||||
bot_configuration_service = BotConfigurationService
|
||||
|
||||
|
||||
Reference in New Issue
Block a user