from typing import Optional, Tuple, List from aiogram import Router, types from aiogram.filters import Command from aiogram.types import ( InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery, ) import json from bot import db from i18n.localization import I18n router = Router() i18n = I18n() LangOption = Tuple[str, str] PeriodOption = Tuple[str, str] LANG_OPTIONS: List[LangOption] = [ ("en", "English"), ("ru", "Русский"), ] PERIOD_OPTIONS: List[PeriodOption] = [ ("week", "week"), ("month", "month"), ("quarter", "quarter"), ("year", "year"), ] async def get_user_locale(user_id: int) -> dict: data = await db.fetch( 'SELECT lang FROM users WHERE user_id = $1', user_id ) if not data: await db.insert( 'INSERT INTO users (user_id) VALUES (?)', user_id ) data = await db.fetch( 'SELECT lang FROM users WHERE user_id = $1', user_id ) return i18n.get_locale(data.get('lang', 'en')) def build_options_keyboard( options: List[Tuple[str, str]], current_value: str, callback_prefix: str, locale: dict, buttons_per_row: int = 2, back_callback: Optional[str] = None, ) -> InlineKeyboardMarkup: buttons: List[List[InlineKeyboardButton]] = [] row: List[InlineKeyboardButton] = [] for code, label_key in options: label = locale.get(label_key, label_key) text = ( f"[X] {label}" if code == current_value else label ) row.append( InlineKeyboardButton( text=text, callback_data=f"{callback_prefix}_{code}" ) ) if len(row) == buttons_per_row: buttons.append(row) row = [] if row: buttons.append(row) if back_callback: buttons.append( [ InlineKeyboardButton( text=locale.get("back", "Back"), callback_data=back_callback, ) ] ) return InlineKeyboardMarkup(inline_keyboard=buttons) def get_chart_toggle_keyboard( chart_enabled: bool, locale: dict ) -> InlineKeyboardMarkup: toggle_text = ( locale.get("chart_disable", "Disable Chart") if chart_enabled else locale.get("chart_enable", "Enable Chart") ) return InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text=toggle_text, callback_data="chart_toggle" ), InlineKeyboardButton( text=locale.get("chart_period", "Chart Period"), callback_data="chart_period", ), InlineKeyboardButton( text=locale.get("setting_backend", "Chart Backend"), callback_data="setting_backend", ), ], [ InlineKeyboardButton( text=locale.get("back", "Back"), callback_data="back_to_settings", ), ], ] ) def markup_to_json( markup: Optional[InlineKeyboardMarkup], ) -> Optional[str]: if markup is None: return None return json.dumps(markup.model_dump(), sort_keys=True) async def safe_edit_message_text( callback: CallbackQuery, new_text: str, new_reply_markup: Optional[InlineKeyboardMarkup] = None, parse_mode: Optional[str] = None, ) -> None: message = callback.message current_text = (message.text or "").strip() new_text_clean = new_text.strip() is_text_same = current_text == new_text_clean is_markup_same = ( markup_to_json(message.reply_markup) == markup_to_json(new_reply_markup) ) if is_text_same and is_markup_same: await callback.answer() return await message.edit_text( new_text, reply_markup=new_reply_markup, parse_mode=parse_mode ) await callback.answer() @router.message(Command("settings")) async def settings_handler(message: types.Message): locale = await get_user_locale(message.from_user.id) settings_keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text=locale.get("setting_chart", "Chart Settings"), callback_data="setting_chart", ), InlineKeyboardButton( text=locale.get("setting_lang", "Language Settings"), callback_data="setting_lang", ), ], ] ) await message.answer( locale.get("settings_title", "Settings"), reply_markup=settings_keyboard ) @router.callback_query(lambda c: c.data == "setting_lang") async def show_language_menu(callback: CallbackQuery): locale = await get_user_locale(callback.from_user.id) data = await db.fetch( 'SELECT lang FROM users WHERE user_id = $1', callback.from_user.id ) current_lang = data.get('lang', 'en') keyboard = build_options_keyboard( options=LANG_OPTIONS, current_value=current_lang, callback_prefix="lang", locale=locale, back_callback="back_to_settings", ) await safe_edit_message_text( callback, locale.get("choose_language"), keyboard ) @router.callback_query(lambda c: c.data and c.data.startswith("lang_")) async def language_selected(callback: CallbackQuery): lang = callback.data.split("_")[1] await db.update( 'UPDATE users SET lang = $1 WHERE user_id = $2', lang, callback.from_user.id, ) locale = i18n.get_locale(lang) keyboard = build_options_keyboard( options=LANG_OPTIONS, current_value=lang, callback_prefix="lang", locale=locale, back_callback="back_to_settings", ) await safe_edit_message_text( callback, locale.get("choose_language"), keyboard ) await callback.answer( locale.get("language_set").format(lang=lang) ) @router.callback_query(lambda c: c.data == "setting_backend") async def show_backend_settings(callback: CallbackQuery): locale = await get_user_locale(callback.from_user.id) data = await db.fetch( 'SELECT chart_backend, lang FROM users WHERE user_id = $1', callback.from_user.id ) current_backend = data.get('chart_backend', 'matplotlib') backend_label = locale.get(current_backend, current_backend) keyboard = build_options_keyboard( options=[("typst", "Typst"), ("matplotlib", "Matplotlib")], current_value=current_backend, callback_prefix="backend", locale=locale, back_callback="back_to_settings", ) await safe_edit_message_text( callback, f"{locale.get('choose_chart_backend', 'Choose Chart Backend')}", keyboard ) @router.callback_query(lambda c: c.data and c.data.startswith("backend_")) async def set_backend(callback: CallbackQuery): backend = callback.data.split("_")[1] await db.update( 'UPDATE users SET chart_backend = $1 WHERE user_id = $2', backend, callback.from_user.id ) locale = await get_user_locale(callback.from_user.id) keyboard = build_options_keyboard( options=[("typst", "Typst"), ("matplotlib", "Matplotlib")], current_value=backend, callback_prefix="backend", locale=locale, back_callback="back_to_settings", ) await safe_edit_message_text( callback, locale.get("choose_chart_backend", "Choose Chart Backend"), keyboard ) @router.callback_query(lambda c: c.data == "back_to_settings") async def back_to_settings(callback: CallbackQuery): locale = await get_user_locale(callback.from_user.id) settings_keyboard = InlineKeyboardMarkup( inline_keyboard=[ [ InlineKeyboardButton( text=locale.get("setting_chart", "Chart Settings"), callback_data="setting_chart", ), InlineKeyboardButton( text=locale.get("setting_lang", "Language Settings"), callback_data="setting_lang", ), InlineKeyboardButton( text=locale.get("setting_backend", "Chart Backend"), callback_data="setting_backend", ), ], ] ) await safe_edit_message_text( callback, locale.get("settings_title", "Settings"), settings_keyboard ) @router.callback_query(lambda c: c.data == "setting_chart") async def show_chart_settings(callback: CallbackQuery): data = await db.fetch( 'SELECT * FROM users WHERE user_id = $1', callback.from_user.id, ) lang = data.get("lang", "en") locale = i18n.get_locale(lang) chart_status = bool(data.get("chart", 1)) period = data.get("chart_period") status_text = locale.get("enabled", "Enabled") \ if chart_status \ else locale.get("disabled", "Disabled") period_text = locale.get(period, period) text = ( f"{locale.get('chart_settings', 'Chart Settings')}\n" f"{locale.get('status', 'Status')}: {status_text}\n" f"{locale.get('period', 'Period')}: {period_text}\n" f"{locale.get('selected_chart_backend')}: {data.get('chart_backend')}" ) keyboard = get_chart_toggle_keyboard(chart_status, locale) await safe_edit_message_text(callback, text, keyboard) @router.callback_query(lambda c: c.data == "chart_toggle") async def toggle_chart(callback: CallbackQuery): data = await db.fetch( 'SELECT chart, lang FROM users WHERE user_id = $1', callback.from_user.id ) lang = data.get("lang", "en") locale = i18n.get_locale(lang) current_status = data.get("chart", True) new_status = not current_status await db.update( 'UPDATE users SET chart = $1 WHERE user_id = $2', new_status, callback.from_user.id ) await show_chart_settings(callback) @router.callback_query(lambda c: c.data == "chart_period") async def change_chart_period(callback: CallbackQuery): data = await db.fetch( 'SELECT chart_period, lang FROM users WHERE user_id = $1', callback.from_user.id ) lang = data.get("lang", "en") locale = i18n.get_locale(lang) current_period = data.get("chart_period") keyboard = build_options_keyboard( options=PERIOD_OPTIONS, current_value=current_period, callback_prefix="period", locale=locale, back_callback="setting_chart", ) await safe_edit_message_text( callback, locale.get("choose_period", "Choose Period"), keyboard ) @router.callback_query(lambda c: c.data and c.data.startswith("period_")) async def set_chart_period(callback: CallbackQuery): period = callback.data.split("_")[1] await db.update( 'UPDATE users SET chart_period = $1 WHERE user_id = $2', period, callback.from_user.id ) locale = await get_user_locale(callback.from_user.id) keyboard = build_options_keyboard( options=PERIOD_OPTIONS, current_value=period, callback_prefix="period", locale=locale, back_callback="setting_chart", ) await safe_edit_message_text( callback, locale.get("choose_period", "Choose Period"), keyboard ) await callback.answer(locale.get("period_set", "Period set to {period}").format(period=period))