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:
Egor
2025-09-25 21:07:30 +03:00
committed by GitHub
2 changed files with 13 additions and 327 deletions

View File

@@ -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:"),

View File

@@ -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