[upd] nested settings

This commit is contained in:
h
2025-03-29 12:13:17 +02:00
parent 0f17c1f314
commit 8acbbb286c
8 changed files with 497 additions and 211 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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