[upd] nested settings
This commit is contained in:
@@ -23,15 +23,15 @@ async def on_input_setting(
|
||||
state: FSMContext,
|
||||
user_settings: FromDishka[UserSettings],
|
||||
):
|
||||
_, _, section, field = callback.data.split(":")
|
||||
_, _, path, field = callback.data.split(":")
|
||||
|
||||
await state.update_data(section=section, field=field)
|
||||
await state.update_data(path=path, field=field)
|
||||
await state.set_state(SettingsStates.waiting_for_input)
|
||||
|
||||
await callback.answer()
|
||||
await bot.send_message(
|
||||
chat_id=callback.from_user.id,
|
||||
text=SettingsMenuGenerator.get_input_prompt(section, field, user_settings),
|
||||
text=SettingsMenuGenerator.get_input_prompt(path, field, user_settings),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
@@ -43,22 +43,32 @@ async def on_input_value(
|
||||
user_settings: FromDishka[UserSettings],
|
||||
):
|
||||
data = await state.get_data()
|
||||
section = data["section"]
|
||||
path = data["path"]
|
||||
field = data["field"]
|
||||
|
||||
await state.clear()
|
||||
|
||||
section_obj = getattr(user_settings, section)
|
||||
if path == "root":
|
||||
setattr(user_settings, field, message.text)
|
||||
|
||||
setattr(section_obj, field, message.text)
|
||||
await UserSettingsDocument.update_setting(
|
||||
message.from_user.id, field, message.text
|
||||
)
|
||||
else:
|
||||
try:
|
||||
section = user_settings.get_setting(path)
|
||||
setattr(section, field, message.text)
|
||||
|
||||
await UserSettingsDocument.update_field(
|
||||
message.from_user.id, section, field, message.text
|
||||
)
|
||||
await UserSettingsDocument.update_setting(
|
||||
message.from_user.id, f"{path}.{field}", message.text
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
await message.answer("Setting not found")
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
"✅ Setting updated!",
|
||||
reply_markup=SettingsMenuGenerator.get_section_menu(
|
||||
section, user_settings, router
|
||||
path, user_settings, router
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
from aiogram import Bot, F, Router, types
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import CallbackQuery
|
||||
from dishka import FromDishka
|
||||
|
||||
from bot.keyboards.settings import SettingsMenuGenerator
|
||||
from bot.modules.settings import UserSettings
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "settings:main")
|
||||
async def on_main_settings(callback: CallbackQuery, bot: Bot):
|
||||
async def on_main_settings(
|
||||
callback: CallbackQuery, bot: Bot, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
await callback.answer()
|
||||
await bot.edit_message_text(
|
||||
"⚙️ Settings",
|
||||
chat_id=callback.from_user.id,
|
||||
message_id=callback.message.message_id,
|
||||
reply_markup=SettingsMenuGenerator.get_main_menu(router),
|
||||
reply_markup=SettingsMenuGenerator.get_main_menu(user_settings, router),
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("settings"))
|
||||
async def on_settings_command(message: types.Message):
|
||||
async def on_settings_command(
|
||||
message: types.Message, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
await message.answer(
|
||||
"⚙️ Settings", reply_markup=SettingsMenuGenerator.get_main_menu()
|
||||
"⚙️ Settings", reply_markup=SettingsMenuGenerator.get_main_menu(user_settings)
|
||||
)
|
||||
|
||||
@@ -12,14 +12,17 @@ router = Router()
|
||||
async def on_section_settings(
|
||||
callback: CallbackQuery, bot: Bot, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
section = callback.data.split(":")[-1]
|
||||
path = callback.data.split(":", 2)[-1]
|
||||
|
||||
section_title = path.split(".")[-1].capitalize()
|
||||
|
||||
await callback.answer()
|
||||
await bot.edit_message_text(
|
||||
f"⚙️ {section.capitalize()} settings",
|
||||
f"⚙️ {section_title} settings",
|
||||
chat_id=callback.from_user.id,
|
||||
message_id=callback.message.message_id,
|
||||
reply_markup=SettingsMenuGenerator.get_section_menu(
|
||||
section, user_settings, router
|
||||
path, user_settings, router
|
||||
),
|
||||
)
|
||||
|
||||
@@ -30,11 +33,19 @@ async def on_section_settings(
|
||||
async def on_select_setting(
|
||||
callback: CallbackQuery, bot: Bot, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
_, mode, section, field = callback.data.split(":")
|
||||
_, mode, path, field = callback.data.split(":")
|
||||
|
||||
section_obj = getattr(user_settings, section)
|
||||
if path == "root":
|
||||
section = user_settings
|
||||
info = section.get_info(field)
|
||||
else:
|
||||
try:
|
||||
section = user_settings.get_setting(path)
|
||||
info = section.get_info(field)
|
||||
except (AttributeError, KeyError):
|
||||
await callback.answer("Setting not found")
|
||||
return
|
||||
|
||||
info = section_obj.get_info(field)
|
||||
if not info:
|
||||
await callback.answer("Setting info not found")
|
||||
return
|
||||
@@ -45,6 +56,6 @@ async def on_select_setting(
|
||||
chat_id=callback.from_user.id,
|
||||
message_id=callback.message.message_id,
|
||||
reply_markup=getattr(SettingsMenuGenerator, f"get_{mode}_menu")(
|
||||
section, field, user_settings, router
|
||||
path, field, user_settings, router
|
||||
),
|
||||
)
|
||||
|
||||
@@ -13,23 +13,37 @@ router = Router()
|
||||
async def on_toggle_setting(
|
||||
callback: CallbackQuery, bot: Bot, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
_, _, section, field = callback.data.split(":")
|
||||
_, _, path, field = callback.data.split(":")
|
||||
|
||||
section_obj = getattr(user_settings, section)
|
||||
if path == "root":
|
||||
section = user_settings
|
||||
current_value = getattr(section, field)
|
||||
new_value = not current_value
|
||||
setattr(section, field, new_value)
|
||||
|
||||
current_value = getattr(section_obj, field)
|
||||
setattr(section_obj, field, not current_value)
|
||||
await UserSettingsDocument.update_setting(
|
||||
callback.from_user.id, field, new_value
|
||||
)
|
||||
else:
|
||||
try:
|
||||
section = user_settings.get_setting(path)
|
||||
current_value = getattr(section, field)
|
||||
new_value = not current_value
|
||||
setattr(section, field, new_value)
|
||||
|
||||
await UserSettingsDocument.update_field(
|
||||
callback.from_user.id, section, field, not current_value
|
||||
)
|
||||
await UserSettingsDocument.update_setting(
|
||||
callback.from_user.id, f"{path}.{field}", new_value
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
await callback.answer("Setting not found")
|
||||
return
|
||||
|
||||
await callback.answer("Setting updated")
|
||||
await bot.edit_message_reply_markup(
|
||||
chat_id=callback.from_user.id,
|
||||
message_id=callback.message.message_id,
|
||||
reply_markup=SettingsMenuGenerator.get_section_menu(
|
||||
section, user_settings, router
|
||||
path, user_settings, router
|
||||
),
|
||||
)
|
||||
|
||||
@@ -38,21 +52,31 @@ async def on_toggle_setting(
|
||||
async def on_set_value(
|
||||
callback: CallbackQuery, bot: Bot, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
_, _, section, field, value = callback.data.split(":")
|
||||
_, _, path, field, value = callback.data.split(":")
|
||||
|
||||
section_obj = getattr(user_settings, section)
|
||||
if path == "root":
|
||||
section = user_settings
|
||||
setattr(section, field, value)
|
||||
|
||||
setattr(section_obj, field, value)
|
||||
await UserSettingsDocument.update_field(
|
||||
callback.from_user.id, section, field, value
|
||||
)
|
||||
await UserSettingsDocument.update_setting(callback.from_user.id, field, value)
|
||||
else:
|
||||
try:
|
||||
section = user_settings.get_setting(path)
|
||||
setattr(section, field, value)
|
||||
|
||||
await UserSettingsDocument.update_setting(
|
||||
callback.from_user.id, f"{path}.{field}", value
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
await callback.answer("Setting not found")
|
||||
return
|
||||
|
||||
await callback.answer("Setting updated")
|
||||
await bot.edit_message_reply_markup(
|
||||
chat_id=callback.from_user.id,
|
||||
message_id=callback.message.message_id,
|
||||
reply_markup=SettingsMenuGenerator.get_select_menu(
|
||||
section, field, user_settings, router
|
||||
path, field, user_settings, router
|
||||
),
|
||||
)
|
||||
|
||||
@@ -61,11 +85,19 @@ async def on_set_value(
|
||||
async def on_move_item(
|
||||
callback: CallbackQuery, bot: Bot, user_settings: FromDishka[UserSettings]
|
||||
):
|
||||
_, _, section, field, index, direction = callback.data.split(":")
|
||||
_, _, path, field, index, direction = callback.data.split(":")
|
||||
index = int(index)
|
||||
|
||||
section_obj = getattr(user_settings, section)
|
||||
current_list = getattr(section_obj, field)
|
||||
if path == "root":
|
||||
section = user_settings
|
||||
current_list = getattr(section, field)
|
||||
else:
|
||||
try:
|
||||
section = user_settings.get_setting(path)
|
||||
current_list = getattr(section, field)
|
||||
except (AttributeError, KeyError):
|
||||
await callback.answer("Setting not found")
|
||||
return
|
||||
|
||||
if direction == "up" and index > 0:
|
||||
current_list[index], current_list[index - 1] = (
|
||||
@@ -78,17 +110,22 @@ async def on_move_item(
|
||||
current_list[index],
|
||||
)
|
||||
|
||||
setattr(section_obj, field, current_list)
|
||||
setattr(section, field, current_list)
|
||||
|
||||
await UserSettingsDocument.update_field(
|
||||
callback.from_user.id, section, field, current_list
|
||||
)
|
||||
if path == "root":
|
||||
await UserSettingsDocument.update_setting(
|
||||
callback.from_user.id, field, current_list
|
||||
)
|
||||
else:
|
||||
await UserSettingsDocument.update_setting(
|
||||
callback.from_user.id, f"{path}.{field}", current_list
|
||||
)
|
||||
|
||||
await callback.answer("Order updated")
|
||||
await bot.edit_message_reply_markup(
|
||||
chat_id=callback.from_user.id,
|
||||
message_id=callback.message.message_id,
|
||||
reply_markup=SettingsMenuGenerator.get_list_menu(
|
||||
section, field, user_settings, router
|
||||
path, field, user_settings, router
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from aiogram import Router
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
|
||||
from bot.keyboards.utils import Paginator
|
||||
from bot.modules.settings import UserSettings
|
||||
from bot.modules.settings.model import BaseSettings, SettingInfo
|
||||
|
||||
|
||||
@@ -13,26 +12,73 @@ class SettingsMenuGenerator:
|
||||
ITEMS_PER_PAGE = 5
|
||||
|
||||
@classmethod
|
||||
def get_main_menu(cls, router: Optional[Router] = None) -> InlineKeyboardMarkup:
|
||||
def get_main_menu(
|
||||
cls, settings: BaseSettings, router: Optional[Router] = None
|
||||
) -> InlineKeyboardMarkup:
|
||||
keyboard = []
|
||||
fields = get_type_hints(UserSettings)
|
||||
fields = get_type_hints(settings.__class__)
|
||||
|
||||
for field_name, field_type in fields.items():
|
||||
if (
|
||||
not isinstance(field_type, type)
|
||||
or not issubclass(field_type, BaseSettings)
|
||||
or field_name.startswith("_")
|
||||
):
|
||||
sorted_fields = sorted(fields.items(), key=lambda x: x[0])
|
||||
|
||||
for field_name, field_type in sorted_fields:
|
||||
if field_name.startswith("_"):
|
||||
continue
|
||||
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=field_name.capitalize(),
|
||||
callback_data=f"settings:section:{field_name}",
|
||||
if hasattr(settings, field_name):
|
||||
field_value = getattr(settings, field_name)
|
||||
|
||||
if isinstance(field_value, BaseSettings):
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=field_name.capitalize(),
|
||||
callback_data=f"settings:section:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
elif not field_name.endswith("_info"):
|
||||
info = settings.get_info(field_name)
|
||||
if info:
|
||||
value = field_value
|
||||
if isinstance(value, bool):
|
||||
value_text = "✅ Enabled" if value else "❌ Disabled"
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}: {value_text}",
|
||||
callback_data=f"settings:toggle:root:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
elif field_type == str and info.options:
|
||||
value_text = info.options.get(value, value)
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}: {value_text}",
|
||||
callback_data=f"settings:select:root:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
elif field_type == List[str]:
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}",
|
||||
callback_data=f"settings:list:root:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}: {value}",
|
||||
callback_data=f"settings:input:root:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
paginator = Paginator(
|
||||
data=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
@@ -46,13 +92,17 @@ class SettingsMenuGenerator:
|
||||
@classmethod
|
||||
def get_section_menu(
|
||||
cls,
|
||||
section_name: str,
|
||||
settings: UserSettings,
|
||||
path: str,
|
||||
settings: BaseSettings,
|
||||
router: Optional[Router] = None,
|
||||
page: int = 0,
|
||||
) -> InlineKeyboardMarkup:
|
||||
section = getattr(settings, section_name)
|
||||
if not section:
|
||||
if path == "root":
|
||||
return cls.get_main_menu(settings, router)
|
||||
|
||||
try:
|
||||
section = settings.get_setting(path)
|
||||
except (AttributeError, KeyError):
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
@@ -63,9 +113,23 @@ class SettingsMenuGenerator:
|
||||
]
|
||||
)
|
||||
|
||||
field_info = section.get_all_info()
|
||||
if not isinstance(section, BaseSettings):
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT, callback_data="settings:main"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
keyboard = []
|
||||
|
||||
fields = get_type_hints(section.__class__)
|
||||
|
||||
field_info = section.get_all_info()
|
||||
|
||||
field_names = [
|
||||
f
|
||||
for f in fields.keys()
|
||||
@@ -76,40 +140,53 @@ class SettingsMenuGenerator:
|
||||
key=lambda f: field_info.get(f, SettingInfo("", "", order=999)).order
|
||||
)
|
||||
|
||||
keyboard = []
|
||||
for field_name in field_names:
|
||||
value = getattr(section, field_name)
|
||||
field_type = fields.get(field_name)
|
||||
|
||||
info = section.get_info(field_name)
|
||||
if info is None:
|
||||
continue
|
||||
title = field_name.replace("_", " ").capitalize()
|
||||
info = SettingInfo(title=title, description=f"Configure {title}")
|
||||
|
||||
field_type = fields[field_name]
|
||||
if field_type == bool:
|
||||
if isinstance(value, BaseSettings):
|
||||
nested_title = (
|
||||
info.title if info and info.title else field_name.capitalize()
|
||||
)
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{nested_title} ➡️",
|
||||
callback_data=f"settings:section:{path}.{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
elif field_type == bool or isinstance(value, bool):
|
||||
value_text = "✅ Enabled" if value else "❌ Disabled"
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}: {value_text}",
|
||||
callback_data=f"settings:toggle:{section_name}:{field_name}",
|
||||
callback_data=f"settings:toggle:{path}:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
elif field_type == str and info.options:
|
||||
elif (field_type == str or isinstance(value, str)) and info.options:
|
||||
value_text = info.options.get(value, value)
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}: {value_text}",
|
||||
callback_data=f"settings:select:{section_name}:{field_name}",
|
||||
callback_data=f"settings:select:{path}:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
elif field_type == List[str]:
|
||||
elif field_type == List[str] or isinstance(value, list):
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}",
|
||||
callback_data=f"settings:list:{section_name}:{field_name}",
|
||||
callback_data=f"settings:list:{path}:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -118,131 +195,109 @@ class SettingsMenuGenerator:
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{info.title}: {value}",
|
||||
callback_data=f"settings:input:{section_name}:{field_name}",
|
||||
callback_data=f"settings:input:{path}:{field_name}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
back_target = "settings:main"
|
||||
if "." in path:
|
||||
parent_path = path.rsplit(".", 1)[0]
|
||||
back_target = f"settings:section:{parent_path}"
|
||||
|
||||
after_data = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT, callback_data="settings:main"
|
||||
)
|
||||
]
|
||||
[InlineKeyboardButton(text=cls.BACK_BUTTON_TEXT, callback_data=back_target)]
|
||||
]
|
||||
|
||||
paginator = Paginator(
|
||||
data=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
after_data=after_data,
|
||||
callback_startswith=f"settings_section_{section_name}_page_",
|
||||
callback_startswith=f"settings_section_{path.replace('.', '_')}_page_",
|
||||
size=cls.ITEMS_PER_PAGE,
|
||||
dp=router,
|
||||
)
|
||||
|
||||
return paginator(current_page=page)
|
||||
|
||||
@classmethod
|
||||
def get_select_menu(
|
||||
cls,
|
||||
section_name: str,
|
||||
field_name: str,
|
||||
settings: UserSettings,
|
||||
path: str,
|
||||
field: str,
|
||||
settings: BaseSettings,
|
||||
router: Optional[Router] = None,
|
||||
) -> InlineKeyboardMarkup:
|
||||
section = getattr(settings, section_name)
|
||||
if path == "root":
|
||||
section = settings
|
||||
else:
|
||||
try:
|
||||
section = settings.get_setting(path)
|
||||
except (AttributeError, KeyError):
|
||||
return cls._get_default_back_menu("settings:main")
|
||||
|
||||
if not section:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT, callback_data="settings:main"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
info = section.get_info(field_name)
|
||||
info = section.get_info(field)
|
||||
if not info or not info.options:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT,
|
||||
callback_data=f"settings:section:{section_name}",
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
return cls._get_default_back_menu(f"settings:section:{path}")
|
||||
|
||||
current_value = getattr(section, field)
|
||||
|
||||
keyboard = []
|
||||
for option_key, option_text in info.options.items():
|
||||
prefix = "✅ " if option_key == getattr(section, field_name) else ""
|
||||
prefix = "✅ " if option_key == current_value else ""
|
||||
keyboard.append(
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=f"{prefix}{option_text}",
|
||||
callback_data=f"settings:set:{section_name}:{field_name}:{option_key}",
|
||||
callback_data=f"settings:set:{path}:{field}:{option_key}",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
after_data = [
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT,
|
||||
callback_data=f"settings:section:{path}",
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
callback_prefix = path.replace(".", "_") if path != "root" else "root"
|
||||
paginator = Paginator(
|
||||
data=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
after_data=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT,
|
||||
callback_data=f"settings:section:{section_name}",
|
||||
)
|
||||
]
|
||||
],
|
||||
callback_startswith=f"settings_select_{section_name}_{field_name}_page_",
|
||||
after_data=after_data,
|
||||
callback_startswith=f"settings_select_{callback_prefix}_{field}_page_",
|
||||
size=cls.ITEMS_PER_PAGE,
|
||||
dp=router,
|
||||
)
|
||||
|
||||
return paginator()
|
||||
|
||||
@classmethod
|
||||
def get_list_menu(
|
||||
cls,
|
||||
section_name: str,
|
||||
field_name: str,
|
||||
settings: UserSettings,
|
||||
path: str,
|
||||
field: str,
|
||||
settings: BaseSettings,
|
||||
router: Optional[Router] = None,
|
||||
) -> InlineKeyboardMarkup:
|
||||
section = getattr(settings, section_name)
|
||||
if not section:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT, callback_data="settings:main"
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
if path == "root":
|
||||
section = settings
|
||||
else:
|
||||
try:
|
||||
section = settings.get_setting(path)
|
||||
except (AttributeError, KeyError):
|
||||
return cls._get_default_back_menu("settings:main")
|
||||
|
||||
info = section.get_info(field_name)
|
||||
info = section.get_info(field)
|
||||
if not info:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT,
|
||||
callback_data=f"settings:section:{section_name}",
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
return cls._get_default_back_menu(f"settings:section:{path}")
|
||||
|
||||
current_list = getattr(section, field_name)
|
||||
current_list = getattr(section, field)
|
||||
if not isinstance(current_list, list):
|
||||
return cls._get_default_back_menu(f"settings:section:{path}")
|
||||
|
||||
keyboard = []
|
||||
for i, item in enumerate(current_list):
|
||||
item_text = info.options.get(item, item) if info.options else item
|
||||
|
||||
row = [
|
||||
InlineKeyboardButton(text=f"{i + 1}. {item_text}", callback_data="_")
|
||||
]
|
||||
@@ -252,14 +307,14 @@ class SettingsMenuGenerator:
|
||||
move_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬆️",
|
||||
callback_data=f"settings:move:{section_name}:{field_name}:{i}:up",
|
||||
callback_data=f"settings:move:{path}:{field}:{i}:up",
|
||||
)
|
||||
)
|
||||
if i < len(current_list) - 1:
|
||||
move_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬇️",
|
||||
callback_data=f"settings:move:{section_name}:{field_name}:{i}:down",
|
||||
callback_data=f"settings:move:{path}:{field}:{i}:down",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -269,31 +324,45 @@ class SettingsMenuGenerator:
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT,
|
||||
callback_data=f"settings:section:{section_name}",
|
||||
callback_data=f"settings:section:{path}",
|
||||
)
|
||||
]
|
||||
]
|
||||
|
||||
callback_prefix = path.replace(".", "_") if path != "root" else "root"
|
||||
paginator = Paginator(
|
||||
data=InlineKeyboardMarkup(inline_keyboard=keyboard),
|
||||
after_data=after_data,
|
||||
callback_startswith=f"settings_list_{section_name}_{field_name}_page_",
|
||||
callback_startswith=f"settings_list_{callback_prefix}_{field}_page_",
|
||||
size=cls.ITEMS_PER_PAGE,
|
||||
dp=router,
|
||||
)
|
||||
|
||||
return paginator()
|
||||
|
||||
@classmethod
|
||||
def get_input_prompt(
|
||||
cls, section_name: str, field_name: str, settings: UserSettings
|
||||
) -> str:
|
||||
section = getattr(settings, section_name)
|
||||
if not section:
|
||||
return "Enter your value:"
|
||||
def get_input_prompt(cls, path: str, field: str, settings: BaseSettings) -> str:
|
||||
if path == "root":
|
||||
section = settings
|
||||
else:
|
||||
try:
|
||||
section = settings.get_setting(path)
|
||||
except (AttributeError, KeyError):
|
||||
return "Enter your value:"
|
||||
|
||||
info = section.get_info(field_name)
|
||||
info = section.get_info(field)
|
||||
if not info:
|
||||
return "Enter your value:"
|
||||
|
||||
return f"<b>{info.title}</b>\n{info.description}\n\nPlease enter your value:"
|
||||
|
||||
@classmethod
|
||||
def _get_default_back_menu(cls, back_target: str) -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup(
|
||||
inline_keyboard=[
|
||||
[
|
||||
InlineKeyboardButton(
|
||||
text=cls.BACK_BUTTON_TEXT, callback_data=back_target
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from typing import ClassVar, Dict, Optional, Unpack
|
||||
from typing import Any, ClassVar, Dict, Generic, Optional, TypeVar, Unpack
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class SettingInfo:
|
||||
def __init__(
|
||||
@@ -35,6 +37,103 @@ class BaseSettings(BaseModel):
|
||||
def get_all_info(self) -> Dict[str, SettingInfo]:
|
||||
return self.__class__._settings_info
|
||||
|
||||
@staticmethod
|
||||
def _get_nested_value(obj: Any, path: str):
|
||||
if not path or path == "root":
|
||||
return obj
|
||||
|
||||
parts = path.split(".", 1)
|
||||
current = parts[0]
|
||||
|
||||
if not hasattr(obj, current):
|
||||
raise AttributeError(f"Object has no attribute '{current}'")
|
||||
|
||||
if len(parts) == 1:
|
||||
return getattr(obj, current)
|
||||
|
||||
return BaseSettings._get_nested_value(getattr(obj, current), parts[1])
|
||||
|
||||
@staticmethod
|
||||
def _set_nested_value(obj: Any, path: str, value: Any):
|
||||
if not path or path == "root":
|
||||
return
|
||||
|
||||
parts = path.split(".", 1)
|
||||
current = parts[0]
|
||||
|
||||
if not hasattr(obj, current):
|
||||
raise AttributeError(f"Object has no attribute '{current}'")
|
||||
|
||||
if len(parts) == 1:
|
||||
setattr(obj, current, value)
|
||||
return
|
||||
|
||||
BaseSettings._set_nested_value(getattr(obj, current), parts[1], value)
|
||||
|
||||
def get_setting(self, path: str) -> Any:
|
||||
return self._get_nested_value(self, path)
|
||||
|
||||
def set_setting(self, path: str, value: Any) -> None:
|
||||
self._set_nested_value(self, path, value)
|
||||
|
||||
def get_nested_info(self, path: str) -> Optional[SettingInfo]:
|
||||
if not path or path == "root":
|
||||
return None
|
||||
|
||||
parts = path.split(".")
|
||||
if len(parts) == 1:
|
||||
return self.get_info(parts[0])
|
||||
|
||||
section_name = parts[0]
|
||||
if not hasattr(self, section_name):
|
||||
return None
|
||||
|
||||
section = getattr(self, section_name)
|
||||
if not isinstance(section, BaseSettings):
|
||||
return None
|
||||
|
||||
return section.get_nested_info(".".join(parts[1:]))
|
||||
|
||||
class Config:
|
||||
populate_by_name = True
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class SettingsAccessor(Generic[T]):
|
||||
def __init__(self, root_settings: BaseSettings, path: str = ""):
|
||||
self._root = root_settings
|
||||
self._path = path
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
full_path = f"{self._path}.{name}" if self._path else name
|
||||
|
||||
value = self._root.get_setting(full_path)
|
||||
|
||||
if isinstance(value, BaseSettings):
|
||||
return SettingsAccessor(self._root, full_path)
|
||||
|
||||
return value
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if name.startswith("_"):
|
||||
super().__setattr__(name, value)
|
||||
return
|
||||
|
||||
full_path = f"{self._path}.{name}" if self._path else name
|
||||
self._root.set_setting(full_path, value)
|
||||
|
||||
|
||||
class AccessibleSettings(BaseSettings):
|
||||
def __init__(self, **data):
|
||||
super().__init__(**data)
|
||||
self._accessor = SettingsAccessor(self)
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
try:
|
||||
return super().__getattr__(name)
|
||||
except AttributeError:
|
||||
if name.startswith("_"):
|
||||
raise
|
||||
if hasattr(self._accessor, name):
|
||||
return getattr(self._accessor, name)
|
||||
raise
|
||||
|
||||
@@ -2,7 +2,68 @@ from typing import ClassVar, List
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from bot.modules.settings.model import BaseSettings, SettingInfo
|
||||
from .model import AccessibleSettings, BaseSettings, SettingInfo
|
||||
|
||||
|
||||
class FormatSettings(BaseSettings):
|
||||
bitrate: str = Field(default=320)
|
||||
bitrate_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Bitrate",
|
||||
description="Audio quality in kbps",
|
||||
options={
|
||||
"128": "Low (128kbps)",
|
||||
"256": "Medium (256kbps)",
|
||||
"320": "High (320kbps)",
|
||||
},
|
||||
order=10,
|
||||
)
|
||||
|
||||
codec: str = Field(default="mp3")
|
||||
codec_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Audio Codec",
|
||||
description="Format to encode audio files",
|
||||
options={"mp3": "MP3", "ogg": "OGG Vorbis", "m4a": "AAC"},
|
||||
order=20,
|
||||
)
|
||||
|
||||
|
||||
class MusicSettings(BaseSettings):
|
||||
format: FormatSettings = Field(default_factory=FormatSettings)
|
||||
format_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Format Settings",
|
||||
description="Audio format configuration",
|
||||
order=10,
|
||||
)
|
||||
|
||||
normalize_volume: bool = Field(default=True)
|
||||
normalize_volume_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Normalize Volume",
|
||||
description="Automatically adjust volume to a standard level",
|
||||
order=30,
|
||||
)
|
||||
|
||||
add_metadata: bool = Field(default=True)
|
||||
add_metadata_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Add Metadata",
|
||||
description="Include artist, album and track information in files",
|
||||
order=40,
|
||||
)
|
||||
|
||||
|
||||
class NotificationSettings(BaseSettings):
|
||||
downloads_complete: bool = Field(default=True)
|
||||
downloads_complete_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Download Notifications",
|
||||
description="Notify when downloads are complete",
|
||||
order=10,
|
||||
)
|
||||
|
||||
new_features: bool = Field(default=True)
|
||||
new_features_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Feature Announcements",
|
||||
description="Receive notifications about new bot features",
|
||||
order=20,
|
||||
)
|
||||
|
||||
|
||||
class SearchSettings(BaseSettings):
|
||||
@@ -21,24 +82,6 @@ class SearchSettings(BaseSettings):
|
||||
order=20,
|
||||
)
|
||||
|
||||
|
||||
class DownloadSettings(BaseSettings):
|
||||
recode_youtube: bool = Field(default=True)
|
||||
recode_youtube_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Recode YouTube",
|
||||
description="Recode when downloading from YouTube to more compatible format (may take some time)",
|
||||
order=10,
|
||||
)
|
||||
|
||||
exact_spotify_match: bool = Field(default=True)
|
||||
exact_spotify_match_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Exact Spotify matches",
|
||||
description="When searching on YouTube from Spotify, show only exact matches",
|
||||
order=20,
|
||||
)
|
||||
|
||||
|
||||
class ProviderSettings(BaseSettings):
|
||||
order: List[str] = Field(default=["youtube", "deezer", "soundcloud", "spotify"])
|
||||
order_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Provider order",
|
||||
@@ -49,22 +92,36 @@ class ProviderSettings(BaseSettings):
|
||||
"soundcloud": "SoundCloud",
|
||||
"spotify": "Spotify",
|
||||
},
|
||||
order=10,
|
||||
order=30,
|
||||
)
|
||||
|
||||
|
||||
class AppearanceSettings(BaseSettings):
|
||||
theme: str = Field(default="default")
|
||||
theme_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Theme",
|
||||
description="Visual appearance of the bot",
|
||||
options={"default": "Default", "compact": "Compact", "emoji": "Emoji Rich"},
|
||||
order=10,
|
||||
)
|
||||
|
||||
|
||||
class UserSettings(BaseSettings):
|
||||
class UserSettings(AccessibleSettings):
|
||||
search: SearchSettings = Field(default_factory=SearchSettings)
|
||||
download: DownloadSettings = Field(default_factory=DownloadSettings)
|
||||
providers: ProviderSettings = Field(default_factory=ProviderSettings)
|
||||
appearance: AppearanceSettings = Field(default_factory=AppearanceSettings)
|
||||
search_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Search Settings",
|
||||
description="Configure search behavior",
|
||||
order=10,
|
||||
)
|
||||
|
||||
music: MusicSettings = Field(default_factory=MusicSettings)
|
||||
music_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Music Settings",
|
||||
description="Configure music playback and downloads",
|
||||
order=20,
|
||||
)
|
||||
|
||||
notifications: NotificationSettings = Field(default_factory=NotificationSettings)
|
||||
notifications_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Notifications",
|
||||
description="Configure notification preferences",
|
||||
order=30,
|
||||
)
|
||||
|
||||
language: str = Field(default="en")
|
||||
language_info: ClassVar[SettingInfo] = SettingInfo(
|
||||
title="Language",
|
||||
description="Bot interface language",
|
||||
options={"en": "English", "es": "Spanish", "fr": "French", "de": "German"},
|
||||
order=40,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from beanie import Document
|
||||
from pydantic import Field
|
||||
@@ -27,27 +27,24 @@ class UserSettingsDocument(Document):
|
||||
await self.save()
|
||||
|
||||
@classmethod
|
||||
async def update_section(cls, user_id: int, section: str, value: Any) -> None:
|
||||
async def update_setting(cls, user_id: int, path: str, value: Any) -> None:
|
||||
doc = await cls.find_one(cls.user_id == user_id)
|
||||
if not doc:
|
||||
doc = cls(user_id=user_id)
|
||||
setattr(doc.settings, section, value)
|
||||
await doc.insert()
|
||||
else:
|
||||
setattr(doc.settings, section, value)
|
||||
await doc.save()
|
||||
|
||||
doc.settings.set_setting(path, value)
|
||||
await doc.save()
|
||||
|
||||
@classmethod
|
||||
async def update_field(
|
||||
cls, user_id: int, section: str, field: str, value: Any
|
||||
) -> None:
|
||||
async def get_setting(cls, user_id: int, path: str) -> Any:
|
||||
doc = await cls.find_one(cls.user_id == user_id)
|
||||
if not doc:
|
||||
doc = cls(user_id=user_id)
|
||||
section_obj = getattr(doc.settings, section)
|
||||
setattr(section_obj, field, value)
|
||||
await doc.insert()
|
||||
else:
|
||||
section_obj = getattr(doc.settings, section)
|
||||
setattr(section_obj, field, value)
|
||||
await doc.save()
|
||||
return UserSettings().get_setting(path)
|
||||
|
||||
return doc.settings.get_setting(path)
|
||||
|
||||
@classmethod
|
||||
async def get_setting_info(cls, path: str) -> Optional[Any]:
|
||||
settings = UserSettings()
|
||||
return settings.get_nested_info(path)
|
||||
|
||||
Reference in New Issue
Block a user