From 159036957a5cc86440e55281ecf2c05e65beef06 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 11:19:21 +0300 Subject: [PATCH 1/8] chore: Added a table with the choice of service to create the graph --- database/schemas/data.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/schemas/data.sql b/database/schemas/data.sql index f63c3b3..6d93e8c 100644 --- a/database/schemas/data.sql +++ b/database/schemas/data.sql @@ -2,5 +2,6 @@ CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, chart BOOLEAN DEFAULT 1, chart_period TEXT DEFAULT 'month', - lang TEXT DEFAULT 'en' + lang TEXT DEFAULT 'en', + chart_backend TEXT DEFAULT 'matplotlib' ); From 81a57e4b9eefaebf714075e2ffb5422fff6d0026 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 11:19:42 +0300 Subject: [PATCH 2/8] chore: Transmitted the receipt of the graph --- functions/create_chart.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/functions/create_chart.py b/functions/create_chart.py index 1308ab2..fbde263 100644 --- a/functions/create_chart.py +++ b/functions/create_chart.py @@ -1,4 +1,5 @@ from http import HTTPStatus +from urllib.parse import urlencode import yaml import aiohttp @@ -8,19 +9,22 @@ config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8')) async def create_chart( from_currency: str, conv_currency: str, - period: str) -> (dict, None): - async with aiohttp.ClientSession( - timeout=aiohttp.ClientTimeout(total=3) - ) as session: - async with session.get( - f'{config["kekkai_instance"]}/api/getChart/{period}/', - params={ - 'from_currency': from_currency, - 'conv_currency': conv_currency - }) as res: + period: str, + backend: str) -> (str, None): + params = { + 'from_currency': from_currency, + 'conv_currency': conv_currency, + 'period': period, + 'backend': backend + } + + base_url = f'{config["kekkai_instance"]}/api/getChart/' + query_string = urlencode(params) + full_url = f'{base_url}?{query_string}' + + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session: + async with session.get(full_url) as res: if not HTTPStatus(res.status).is_success: return None - - data = await res.json() - - return data.get('detail', None) + + return full_url From f98943674fa42fddc193f85f9c18c674de7bab27 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 11:20:16 +0300 Subject: [PATCH 3/8] chore: Added transfer of data about the service of chart creation to the query --- commands/currency.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commands/currency.py b/commands/currency.py index 32a93d7..bd5ac23 100644 --- a/commands/currency.py +++ b/commands/currency.py @@ -22,8 +22,7 @@ async def currency(query: types.InlineQuery) -> None: get_bot = await bot.get_me() data = await db.fetch( - 'SELECT lang, chart, chart_period ' - 'FROM users WHERE user_id = ?', + 'SELECT * FROM users WHERE user_id = ?', query.from_user.id ) @@ -95,7 +94,8 @@ async def currency(query: types.InlineQuery) -> None: chart = await create_chart( from_currency, conv_currency, - data.get('chart_period', 'month') + data.get('chart_period', 'month'), + data.get('chart_backend', 'matplotlib') ) message = ( From f67d3fb2a314c911c41341c27e9b6d3cbf0e0251 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 14:57:55 +0300 Subject: [PATCH 4/8] fix: Fixed image loading by adding time to query params --- functions/create_chart.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/functions/create_chart.py b/functions/create_chart.py index fbde263..f5371fa 100644 --- a/functions/create_chart.py +++ b/functions/create_chart.py @@ -1,5 +1,6 @@ from http import HTTPStatus from urllib.parse import urlencode +import time import yaml import aiohttp @@ -15,8 +16,12 @@ async def create_chart( 'from_currency': from_currency, 'conv_currency': conv_currency, 'period': period, - 'backend': backend + 'backend': backend, + 'time_unique': time.time() } + + # Without time_unqiue Telegram does not want to load the image + # Probably because of some kind of caching, but it's disabled. base_url = f'{config["kekkai_instance"]}/api/getChart/' query_string = urlencode(params) From 8a59cdb62823910a891c2858ccdcd4c5cfa87302 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 15:30:14 +0300 Subject: [PATCH 5/8] chore: moved the backend button to chart settings --- commands/settings.py | 110 ++++++++++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 28 deletions(-) diff --git a/commands/settings.py b/commands/settings.py index 7877914..ba69bd4 100644 --- a/commands/settings.py +++ b/commands/settings.py @@ -74,7 +74,7 @@ def build_options_keyboard( buttons.append( [ InlineKeyboardButton( - text=locale.get("back"), + text=locale.get("back", "Back"), callback_data=back_callback, ) ] @@ -86,9 +86,9 @@ def get_chart_toggle_keyboard( chart_enabled: bool, locale: dict ) -> InlineKeyboardMarkup: toggle_text = ( - locale.get("chart_disable") + locale.get("chart_disable", "Disable Chart") if chart_enabled - else locale.get("chart_enable") + else locale.get("chart_enable", "Enable Chart") ) return InlineKeyboardMarkup( inline_keyboard=[ @@ -97,13 +97,17 @@ def get_chart_toggle_keyboard( text=toggle_text, callback_data="chart_toggle" ), InlineKeyboardButton( - text=locale.get("chart_period"), + 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"), + text=locale.get("back", "Back"), callback_data="back_to_settings", ), ], @@ -153,11 +157,11 @@ async def settings_handler(message: types.Message): inline_keyboard=[ [ InlineKeyboardButton( - text=locale.get("setting_chart"), + text=locale.get("setting_chart", "Chart Settings"), callback_data="setting_chart", ), InlineKeyboardButton( - text=locale.get("setting_lang"), + text=locale.get("setting_lang", "Language Settings"), callback_data="setting_lang", ), ], @@ -165,10 +169,10 @@ async def settings_handler(message: types.Message): ) await message.answer( - locale.get("settings_title"), reply_markup=settings_keyboard + 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) @@ -217,6 +221,58 @@ async def language_selected(callback: CallbackQuery): ) +@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) @@ -225,26 +281,30 @@ async def back_to_settings(callback: CallbackQuery): inline_keyboard=[ [ InlineKeyboardButton( - text=locale.get("setting_chart"), + text=locale.get("setting_chart", "Chart Settings"), callback_data="setting_chart", ), InlineKeyboardButton( - text=locale.get("setting_lang"), + 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_keyboard + 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 chart, chart_period, lang FROM users WHERE user_id = $1', + 'SELECT * FROM users WHERE user_id = $1', callback.from_user.id, ) lang = data.get("lang", "en") @@ -253,15 +313,16 @@ async def show_chart_settings(callback: CallbackQuery): chart_status = bool(data.get("chart", 1)) period = data.get("chart_period") - status_text = locale.get("enabled") \ + status_text = locale.get("enabled", "Enabled") \ if chart_status \ - else locale.get("disabled") + else locale.get("disabled", "Disabled") period_text = locale.get(period, period) text = ( - f"{locale.get('chart_settings')}\n" - f"{locale.get('status')}: {status_text}\n" - f"{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) @@ -285,13 +346,6 @@ async def toggle_chart(callback: CallbackQuery): new_status, callback.from_user.id ) - await callback.answer( - locale.get( - f"chart_now_{'enabled' if new_status else 'disabled'}", - f"Chart now {'enabled' if new_status else 'disabled'}", - ) - ) - await show_chart_settings(callback) @@ -316,7 +370,7 @@ async def change_chart_period(callback: CallbackQuery): await safe_edit_message_text( callback, - locale.get("choose_period"), + locale.get("choose_period", "Choose Period"), keyboard ) @@ -341,7 +395,7 @@ async def set_chart_period(callback: CallbackQuery): await safe_edit_message_text( callback, - locale.get("choose_period"), + locale.get("choose_period", "Choose Period"), keyboard ) - await callback.answer(locale.get("period_set").format(period=period)) + await callback.answer(locale.get("period_set", "Period set to {period}").format(period=period)) From ff6f36e6ae2f287fe77d563d2d038490fc694a6a Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 15:49:41 +0300 Subject: [PATCH 6/8] chore: If some line in the localization is not present now, it will be automatically taken from the English localization. Localization is now obtained through the key --- commands/settings.py | 49 ++++++++++++++++++++++---------------------- i18n/localization.py | 10 ++++++++- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/commands/settings.py b/commands/settings.py index ba69bd4..c76f629 100644 --- a/commands/settings.py +++ b/commands/settings.py @@ -74,7 +74,7 @@ def build_options_keyboard( buttons.append( [ InlineKeyboardButton( - text=locale.get("back", "Back"), + text=locale["back"], callback_data=back_callback, ) ] @@ -86,9 +86,9 @@ def get_chart_toggle_keyboard( chart_enabled: bool, locale: dict ) -> InlineKeyboardMarkup: toggle_text = ( - locale.get("chart_disable", "Disable Chart") + locale["chart_disable"] if chart_enabled - else locale.get("chart_enable", "Enable Chart") + else locale["chart_enable"] ) return InlineKeyboardMarkup( inline_keyboard=[ @@ -97,17 +97,17 @@ def get_chart_toggle_keyboard( text=toggle_text, callback_data="chart_toggle" ), InlineKeyboardButton( - text=locale.get("chart_period", "Chart Period"), + text=locale["chart_period"], callback_data="chart_period", ), InlineKeyboardButton( - text=locale.get("setting_backend", "Chart Backend"), + text=locale["setting_backend"], callback_data="setting_backend", ), ], [ InlineKeyboardButton( - text=locale.get("back", "Back"), + text=locale["back"], callback_data="back_to_settings", ), ], @@ -157,11 +157,11 @@ async def settings_handler(message: types.Message): inline_keyboard=[ [ InlineKeyboardButton( - text=locale.get("setting_chart", "Chart Settings"), + text=locale["setting_chart"], callback_data="setting_chart", ), InlineKeyboardButton( - text=locale.get("setting_lang", "Language Settings"), + text=locale["setting_lang"], callback_data="setting_lang", ), ], @@ -169,7 +169,7 @@ async def settings_handler(message: types.Message): ) await message.answer( - locale.get("settings_title", "Settings"), + locale["settings_title"], reply_markup=settings_keyboard ) @@ -192,7 +192,7 @@ async def show_language_menu(callback: CallbackQuery): ) await safe_edit_message_text( - callback, locale.get("choose_language"), keyboard + callback, locale["choose_language"], keyboard ) @router.callback_query(lambda c: c.data and c.data.startswith("lang_")) @@ -214,10 +214,10 @@ async def language_selected(callback: CallbackQuery): ) await safe_edit_message_text( - callback, locale.get("choose_language"), keyboard + callback, locale["choose_language"], keyboard ) await callback.answer( - locale.get("language_set").format(lang=lang) + locale["language_set"].format(lang=lang) ) @@ -229,7 +229,7 @@ async def show_backend_settings(callback: CallbackQuery): 'SELECT chart_backend, lang FROM users WHERE user_id = $1', callback.from_user.id ) - current_backend = data.get('chart_backend', 'matplotlib') + current_backend = data['chart_backend'] backend_label = locale.get(current_backend, current_backend) keyboard = build_options_keyboard( @@ -242,7 +242,7 @@ async def show_backend_settings(callback: CallbackQuery): await safe_edit_message_text( callback, - f"{locale.get('choose_chart_backend', 'Choose Chart Backend')}", + f"{locale['choose_chart_backend']}", keyboard ) @@ -268,7 +268,7 @@ async def set_backend(callback: CallbackQuery): await safe_edit_message_text( callback, - locale.get("choose_chart_backend", "Choose Chart Backend"), + locale["choose_chart_backend"], keyboard ) @@ -281,15 +281,15 @@ async def back_to_settings(callback: CallbackQuery): inline_keyboard=[ [ InlineKeyboardButton( - text=locale.get("setting_chart", "Chart Settings"), + text=locale["setting_chart"], callback_data="setting_chart", ), InlineKeyboardButton( - text=locale.get("setting_lang", "Language Settings"), + text=locale["setting_lang"], callback_data="setting_lang", ), InlineKeyboardButton( - text=locale.get("setting_backend", "Chart Backend"), + text=locale["setting_backend"], callback_data="setting_backend", ), ], @@ -319,10 +319,10 @@ async def show_chart_settings(callback: CallbackQuery): 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')}" + f"{locale['chart_settings']}\n" + f"{locale['status']}: {status_text}\n" + f"{locale['period']}: {period_text}\n" + f"{locale['selected_chart_backend']}: {data.get('chart_backend')}" ) keyboard = get_chart_toggle_keyboard(chart_status, locale) @@ -370,7 +370,7 @@ async def change_chart_period(callback: CallbackQuery): await safe_edit_message_text( callback, - locale.get("choose_period", "Choose Period"), + locale["choose_period"], keyboard ) @@ -395,7 +395,6 @@ async def set_chart_period(callback: CallbackQuery): await safe_edit_message_text( callback, - locale.get("choose_period", "Choose Period"), + locale["choose_period"], keyboard ) - await callback.answer(locale.get("period_set", "Period set to {period}").format(period=period)) diff --git a/i18n/localization.py b/i18n/localization.py index 5cdd44d..b8c1e30 100644 --- a/i18n/localization.py +++ b/i18n/localization.py @@ -21,4 +21,12 @@ class I18n: def get_locale(self, lang: str | None = None) -> dict: """Return the whole dictionary for one language (fallback → default_lang).""" lang = (lang or self.default_lang).lower()[:2] - return self.translations.get(lang, self.translations[self.default_lang]) + lang_dict = self.translations.get(lang, self.translations.get(self.default_lang, {})) + + fallback_dict = self.translations.get(self.default_lang, {}) + merged_dict = { + key: lang_dict.get(key, fallback_dict.get(key, key)) + for key in set(lang_dict) | set(fallback_dict) + } + + return merged_dict \ No newline at end of file From cd40332ef60b8187af6bb83b1e37dd5637e7e233 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 15:49:58 +0300 Subject: [PATCH 7/8] chore: Added localization --- i18n/locales/en.yaml | 3 +++ i18n/locales/ru.yaml | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 345b5f8..646100d 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -40,3 +40,6 @@ week: "Week" month: "Month" quarter: "Quarter" year: "Year" +setting_backend: "Chart backend" +choose_chart_backend: "Choose chart backend for create charts" +selected_chart_backend: "Backend for graphs" \ No newline at end of file diff --git a/i18n/locales/ru.yaml b/i18n/locales/ru.yaml index 02abac0..08624ea 100644 --- a/i18n/locales/ru.yaml +++ b/i18n/locales/ru.yaml @@ -39,4 +39,7 @@ period_set: "Период графика установлен: {period}" week: "Неделя" month: "Месяц" quarter: "Квартал" -year: "Год" \ No newline at end of file +year: "Год" +setting_backend: "Бэкенд графиков" +choose_chart_backend: "Выберите бэкэнд для создания графиков" +selected_chart_backend: "Бэкенд для графиков" \ No newline at end of file From 3d1bacf85b8ea52ae65f4f6a633d45cacd74e262 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 6 Jun 2025 15:54:11 +0300 Subject: [PATCH 8/8] chore: autoformatting code and sorting import --- bot.py | 12 ++-- commands/currency.py | 48 +++++--------- commands/settings.py | 134 ++++++++++++++------------------------ commands/start.py | 33 +++++----- database/server.py | 22 ++++--- functions/convert.py | 69 ++++++++++---------- functions/create_chart.py | 31 +++++---- i18n/localization.py | 9 ++- main.py | 28 ++++---- utils/format_number.py | 14 ++-- utils/inline_query.py | 6 +- 11 files changed, 177 insertions(+), 229 deletions(-) diff --git a/bot.py b/bot.py index dfe23f1..3c7e0c8 100644 --- a/bot.py +++ b/bot.py @@ -1,13 +1,13 @@ +import yaml from aiogram import Bot from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode -import yaml from database.server import Database -db = Database('/data/shirino.db') -config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8')) +db = Database("/data/shirino.db") +config = yaml.safe_load(open("../config.yaml", "r", encoding="utf-8")) bot = Bot( - token=config['telegram_token'], - default=DefaultBotProperties(parse_mode=ParseMode.HTML) - ) \ No newline at end of file + token=config["telegram_token"], + default=DefaultBotProperties(parse_mode=ParseMode.HTML), +) diff --git a/commands/currency.py b/commands/currency.py index bd5ac23..b53afe2 100644 --- a/commands/currency.py +++ b/commands/currency.py @@ -1,14 +1,14 @@ import hashlib -from aiogram import types, Router +from aiogram import Router, types from aiogram.filters import Command from bot import bot, db from functions.convert import Converter from functions.create_chart import create_chart +from i18n.localization import I18n from utils.format_number import format_number from utils.inline_query import reply -from i18n.localization import I18n router = Router() i18n = I18n() @@ -21,45 +21,38 @@ async def currency(query: types.InlineQuery) -> None: result_id = hashlib.md5(text.encode()).hexdigest() get_bot = await bot.get_me() - data = await db.fetch( - 'SELECT * FROM users WHERE user_id = ?', - query.from_user.id - ) + data = await db.fetch("SELECT * FROM users WHERE user_id = ?", query.from_user.id) - lang = data.get('lang') + lang = data.get("lang") locale = i18n.get_locale(lang) - currency_example = locale["currency_example"].format( - bot_username=get_bot.username - ) + currency_example = locale["currency_example"].format(bot_username=get_bot.username) if len(args) < 2: await reply( result_id, [(locale["error_not_enough_args"], currency_example, None, None)], - query + query, ) return conv = Converter() - from_currency, conv_currency = '', '' + from_currency, conv_currency = "", "" if len(args) == 3: try: - conv.amount = float(args[0].replace(',', '.')) + conv.amount = float(args[0].replace(",", ".")) if conv.amount < 0: await reply( - result_id, - [(locale["error_negative_amount"], None, None)], - query + result_id, [(locale["error_negative_amount"], None, None)], query ) return except ValueError: await reply( result_id, [(locale["error_invalid_number"], currency_example, None, None)], - query + query, ) return from_currency = args[1] @@ -81,21 +74,17 @@ async def currency(query: types.InlineQuery) -> None: try: await conv.convert() except RuntimeError: - await reply( - result_id, - [(locale["error_currency_rate"], None, None)], - query - ) + await reply(result_id, [(locale["error_currency_rate"], None, None)], query) return chart = None - if bool(data.get('chart', 1)): + if bool(data.get("chart", 1)): chart = await create_chart( from_currency, conv_currency, - data.get('chart_period', 'month'), - data.get('chart_backend', 'matplotlib') + data.get("chart_period", "month"), + data.get("chart_backend", "matplotlib"), ) message = ( @@ -105,13 +94,6 @@ async def currency(query: types.InlineQuery) -> None: results = [(message, None, None)] if chart: - results.insert( - 0, - ( - message, - None, - chart - ) - ) + results.insert(0, (message, None, chart)) await reply(result_id, results, query) diff --git a/commands/settings.py b/commands/settings.py index c76f629..b57b6c1 100644 --- a/commands/settings.py +++ b/commands/settings.py @@ -1,12 +1,10 @@ -from typing import Optional, Tuple, List +import json +from typing import List, Optional, Tuple + from aiogram import Router, types from aiogram.filters import Command -from aiogram.types import ( - InlineKeyboardMarkup, - InlineKeyboardButton, - CallbackQuery, -) -import json +from aiogram.types import (CallbackQuery, InlineKeyboardButton, + InlineKeyboardMarkup) from bot import db from i18n.localization import I18n @@ -31,17 +29,11 @@ PERIOD_OPTIONS: List[PeriodOption] = [ async def get_user_locale(user_id: int) -> dict: - data = await db.fetch( - 'SELECT lang FROM users WHERE user_id = $1', user_id - ) + 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')) + 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( @@ -57,13 +49,9 @@ def build_options_keyboard( for code, label_key in options: label = locale.get(label_key, label_key) - text = ( - f"[X] {label}" if code == current_value else label - ) + text = f"[X] {label}" if code == current_value else label row.append( - InlineKeyboardButton( - text=text, callback_data=f"{callback_prefix}_{code}" - ) + InlineKeyboardButton(text=text, callback_data=f"{callback_prefix}_{code}") ) if len(row) == buttons_per_row: buttons.append(row) @@ -85,17 +73,11 @@ def build_options_keyboard( def get_chart_toggle_keyboard( chart_enabled: bool, locale: dict ) -> InlineKeyboardMarkup: - toggle_text = ( - locale["chart_disable"] - if chart_enabled - else locale["chart_enable"] - ) + toggle_text = locale["chart_disable"] if chart_enabled else locale["chart_enable"] return InlineKeyboardMarkup( inline_keyboard=[ [ - InlineKeyboardButton( - text=toggle_text, callback_data="chart_toggle" - ), + InlineKeyboardButton(text=toggle_text, callback_data="chart_toggle"), InlineKeyboardButton( text=locale["chart_period"], callback_data="chart_period", @@ -134,9 +116,8 @@ async def safe_edit_message_text( 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) + is_markup_same = markup_to_json(message.reply_markup) == markup_to_json( + new_reply_markup ) if is_text_same and is_markup_same: @@ -168,20 +149,17 @@ async def settings_handler(message: types.Message): ] ) - await message.answer( - locale["settings_title"], - reply_markup=settings_keyboard - ) + await message.answer(locale["settings_title"], 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 + "SELECT lang FROM users WHERE user_id = $1", callback.from_user.id ) - current_lang = data.get('lang', 'en') + current_lang = data.get("lang", "en") keyboard = build_options_keyboard( options=LANG_OPTIONS, @@ -191,15 +169,14 @@ async def show_language_menu(callback: CallbackQuery): back_callback="back_to_settings", ) - await safe_edit_message_text( - callback, locale["choose_language"], keyboard - ) + await safe_edit_message_text(callback, locale["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', + "UPDATE users SET lang = $1 WHERE user_id = $2", lang, callback.from_user.id, ) @@ -213,12 +190,8 @@ async def language_selected(callback: CallbackQuery): back_callback="back_to_settings", ) - await safe_edit_message_text( - callback, locale["choose_language"], keyboard - ) - await callback.answer( - locale["language_set"].format(lang=lang) - ) + await safe_edit_message_text(callback, locale["choose_language"], keyboard) + await callback.answer(locale["language_set"].format(lang=lang)) @router.callback_query(lambda c: c.data == "setting_backend") @@ -226,10 +199,10 @@ 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 + "SELECT chart_backend, lang FROM users WHERE user_id = $1", + callback.from_user.id, ) - current_backend = data['chart_backend'] + current_backend = data["chart_backend"] backend_label = locale.get(current_backend, current_backend) keyboard = build_options_keyboard( @@ -241,9 +214,7 @@ async def show_backend_settings(callback: CallbackQuery): ) await safe_edit_message_text( - callback, - f"{locale['choose_chart_backend']}", - keyboard + callback, f"{locale['choose_chart_backend']}", keyboard ) @@ -252,8 +223,9 @@ 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 + "UPDATE users SET chart_backend = $1 WHERE user_id = $2", + backend, + callback.from_user.id, ) locale = await get_user_locale(callback.from_user.id) @@ -266,11 +238,7 @@ async def set_backend(callback: CallbackQuery): back_callback="back_to_settings", ) - await safe_edit_message_text( - callback, - locale["choose_chart_backend"], - keyboard - ) + await safe_edit_message_text(callback, locale["choose_chart_backend"], keyboard) @router.callback_query(lambda c: c.data == "back_to_settings") @@ -304,7 +272,7 @@ async def back_to_settings(callback: CallbackQuery): @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', + "SELECT * FROM users WHERE user_id = $1", callback.from_user.id, ) lang = data.get("lang", "en") @@ -313,9 +281,11 @@ async def show_chart_settings(callback: CallbackQuery): 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") + status_text = ( + locale.get("enabled", "Enabled") + if chart_status + else locale.get("disabled", "Disabled") + ) period_text = locale.get(period, period) text = ( @@ -332,8 +302,7 @@ async def show_chart_settings(callback: CallbackQuery): @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 + "SELECT chart, lang FROM users WHERE user_id = $1", callback.from_user.id ) lang = data.get("lang", "en") locale = i18n.get_locale(lang) @@ -342,8 +311,9 @@ async def toggle_chart(callback: CallbackQuery): new_status = not current_status await db.update( - 'UPDATE users SET chart = $1 WHERE user_id = $2', - new_status, callback.from_user.id + "UPDATE users SET chart = $1 WHERE user_id = $2", + new_status, + callback.from_user.id, ) await show_chart_settings(callback) @@ -352,8 +322,7 @@ async def toggle_chart(callback: CallbackQuery): @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 + "SELECT chart_period, lang FROM users WHERE user_id = $1", callback.from_user.id ) lang = data.get("lang", "en") locale = i18n.get_locale(lang) @@ -368,19 +337,16 @@ async def change_chart_period(callback: CallbackQuery): back_callback="setting_chart", ) - await safe_edit_message_text( - callback, - locale["choose_period"], - keyboard - ) + await safe_edit_message_text(callback, locale["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 + "UPDATE users SET chart_period = $1 WHERE user_id = $2", + period, + callback.from_user.id, ) locale = await get_user_locale(callback.from_user.id) @@ -393,8 +359,4 @@ async def set_chart_period(callback: CallbackQuery): back_callback="setting_chart", ) - await safe_edit_message_text( - callback, - locale["choose_period"], - keyboard - ) + await safe_edit_message_text(callback, locale["choose_period"], keyboard) diff --git a/commands/start.py b/commands/start.py index 9ec6687..696cf5e 100644 --- a/commands/start.py +++ b/commands/start.py @@ -1,26 +1,28 @@ -from aiogram import types, Router -from aiogram.filters import CommandStart import re +from aiogram import Router, types +from aiogram.filters import CommandStart + from bot import bot, db from i18n.localization import I18n router = Router() i18n = I18n() + def escape_md_v2(text: str) -> str: - return re.sub(r'([_*\[\]()~#+\-=|{}.!\\])', r'\\\1', text) + return re.sub(r"([_*\[\]()~#+\-=|{}.!\\])", r"\\\1", text) + @router.message(CommandStart()) async def start(message: types.Message) -> None: get_bot = await bot.get_me() data = await db.fetch( - 'SELECT lang FROM users WHERE user_id = $1', - message.from_user.id + "SELECT lang FROM users WHERE user_id = $1", message.from_user.id ) - locale = i18n.get_locale(data.get('lang')) + locale = i18n.get_locale(data.get("lang")) raw_template = locale.get("start_message") raw_text = raw_template.format(bot_username=get_bot.username) @@ -28,15 +30,14 @@ async def start(message: types.Message) -> None: button_text = locale.get("source_code_button") - keyboard = types.InlineKeyboardMarkup(inline_keyboard=[ - [types.InlineKeyboardButton( - text=button_text, - url="https://github.com/redume/shirino") + keyboard = types.InlineKeyboardMarkup( + inline_keyboard=[ + [ + types.InlineKeyboardButton( + text=button_text, url="https://github.com/redume/shirino" + ) + ] ] - ]) - - await message.reply( - text, - parse_mode="MarkdownV2", - reply_markup=keyboard ) + + await message.reply(text, parse_mode="MarkdownV2", reply_markup=keyboard) diff --git a/database/server.py b/database/server.py index b7ec3f6..e200174 100644 --- a/database/server.py +++ b/database/server.py @@ -1,12 +1,13 @@ import json from datetime import date, datetime from pathlib import Path -from typing import Optional, List, Dict, Any +from typing import Any, Dict, List, Optional import aiosqlite import yaml -config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8')) +config = yaml.safe_load(open("../config.yaml", "r", encoding="utf-8")) + def custom_encoder(obj): """ @@ -19,6 +20,7 @@ def custom_encoder(obj): return obj.isoformat() raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable") + class Database: """ Asynchronous SQLite database handler using aiosqlite. @@ -65,16 +67,15 @@ class Database: self.db_path = db_path self.conn: Optional[aiosqlite.Connection] = None - async def _create_table(self) -> None: """ Create table from SQL file using aiosqlite. - Reads SQL commands from 'schemas/data.sql' + Reads SQL commands from 'schemas/data.sql' and executes them as a script. """ sql_file = Path(__file__).parent / "schemas" / "data.sql" - sql = sql_file.read_text(encoding='utf-8') + sql = sql_file.read_text(encoding="utf-8") async with self.conn.execute("BEGIN"): await self.conn.executescript(sql) @@ -139,12 +140,13 @@ class Database: async with self.conn.execute(query, args) as cursor: rows = await cursor.fetchall() - return json.loads( - json.dumps( - [dict(row) for row in rows], - default=custom_encoder + return ( + json.loads( + json.dumps([dict(row) for row in rows], default=custom_encoder) ) - ) if rows else [] + if rows + else [] + ) async def insert(self, query: str, *args) -> Dict[str, Any]: """ diff --git a/functions/convert.py b/functions/convert.py index e7b2ad4..5638968 100644 --- a/functions/convert.py +++ b/functions/convert.py @@ -9,14 +9,15 @@ import yaml from utils.format_number import format_number -config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8')) +config = yaml.safe_load(open("../config.yaml", "r", encoding="utf-8")) + class Converter: def __init__(self): self.amount: float = 1.0 self.conv_amount: float = 0.0 - self.from_currency: str = '' - self.conv_currency: str = '' + self.from_currency: str = "" + self.conv_currency: str = "" async def convert(self) -> None: if not await self.kekkai(): @@ -27,68 +28,66 @@ class Converter: async def get_lastdate(self) -> str: async with aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=3) - ) as session: - async with session.get( - f"{config['kekkai_instance']}/api/metadata" - ) as res: + ) as session: + async with session.get(f"{config['kekkai_instance']}/api/metadata") as res: if not HTTPStatus(res.status).is_success: - return ( - datetime.now() - timedelta(1) - ).strftime('%Y-%m-%d') + return (datetime.now() - timedelta(1)).strftime("%Y-%m-%d") data = await res.json() return data.get( - 'last_date', - (datetime.now() - timedelta(1)).strftime('%Y-%m-%d') - ) + "last_date", (datetime.now() - timedelta(1)).strftime("%Y-%m-%d") + ) async def kekkai(self) -> bool: date = await self.get_lastdate() - async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session: - async with session.get(f'{config['kekkai_instance']}/api/getRate/', params={ - 'from_currency': self.from_currency, - 'conv_currency': self.conv_currency, - 'date': date, - 'conv_amount': self.amount - }) as res: + async with aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=3) + ) as session: + async with session.get( + f"{config['kekkai_instance']}/api/getRate/", + params={ + "from_currency": self.from_currency, + "conv_currency": self.conv_currency, + "date": date, + "conv_amount": self.amount, + }, + ) as res: if not HTTPStatus(res.status).is_success: return False data = await res.json() - self.conv_amount = data.get('conv_amount', 0.0) + self.conv_amount = data.get("conv_amount", 0.0) return True async def ddg(self) -> None: async with aiohttp.ClientSession( timeout=aiohttp.ClientTimeout(total=3) - ) as session: + ) as session: async with session.get( - 'https://duckduckgo.com/js/spice/currency/' - f'{self.amount}/{self.from_currency}/{self.conv_currency}' - ) as res: + "https://duckduckgo.com/js/spice/currency/" + f"{self.amount}/{self.from_currency}/{self.conv_currency}" + ) as res: data_text = await res.text() - data = json.loads(re.findall(r'\(\s*(.*)\s*\);$', data_text)[0]) + data = json.loads(re.findall(r"\(\s*(.*)\s*\);$", data_text)[0]) - for key in ['terms', 'privacy', 'timestamp']: + for key in ["terms", "privacy", "timestamp"]: data.pop(key, None) - if not data.get('to'): + if not data.get("to"): raise RuntimeError( - 'Failed to get the exchange rate from DuckDuckGo' - ) + "Failed to get the exchange rate from DuckDuckGo" + ) - conv = data.get('to')[0] - conv_amount = conv.get('mid') + conv = data.get("to")[0] + conv_amount = conv.get("mid") if conv_amount is None: - raise RuntimeError( - 'Error when converting currency via DuckDuckGo' - ) + raise RuntimeError("Error when converting currency via DuckDuckGo") self.conv_amount = float(conv_amount) diff --git a/functions/create_chart.py b/functions/create_chart.py index f5371fa..fca6143 100644 --- a/functions/create_chart.py +++ b/functions/create_chart.py @@ -1,35 +1,34 @@ +import time from http import HTTPStatus from urllib.parse import urlencode -import time -import yaml import aiohttp +import yaml + +config = yaml.safe_load(open("../config.yaml", "r", encoding="utf-8")) -config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8')) async def create_chart( - from_currency: str, - conv_currency: str, - period: str, - backend: str) -> (str, None): + from_currency: str, conv_currency: str, period: str, backend: str +) -> (str, None): params = { - 'from_currency': from_currency, - 'conv_currency': conv_currency, - 'period': period, - 'backend': backend, - 'time_unique': time.time() + "from_currency": from_currency, + "conv_currency": conv_currency, + "period": period, + "backend": backend, + "time_unique": time.time(), } - # Without time_unqiue Telegram does not want to load the image + # Without time_unqiue Telegram does not want to load the image # Probably because of some kind of caching, but it's disabled. - + base_url = f'{config["kekkai_instance"]}/api/getChart/' query_string = urlencode(params) - full_url = f'{base_url}?{query_string}' + full_url = f"{base_url}?{query_string}" async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session: async with session.get(full_url) as res: if not HTTPStatus(res.status).is_success: return None - + return full_url diff --git a/i18n/localization.py b/i18n/localization.py index b8c1e30..0547f89 100644 --- a/i18n/localization.py +++ b/i18n/localization.py @@ -1,6 +1,7 @@ -import yaml from pathlib import Path +import yaml + class I18n: """Load every YAML file in i18n/locales and let you pull out a single-language dict.""" @@ -21,7 +22,9 @@ class I18n: def get_locale(self, lang: str | None = None) -> dict: """Return the whole dictionary for one language (fallback → default_lang).""" lang = (lang or self.default_lang).lower()[:2] - lang_dict = self.translations.get(lang, self.translations.get(self.default_lang, {})) + lang_dict = self.translations.get( + lang, self.translations.get(self.default_lang, {}) + ) fallback_dict = self.translations.get(self.default_lang, {}) merged_dict = { @@ -29,4 +32,4 @@ class I18n: for key in set(lang_dict) | set(fallback_dict) } - return merged_dict \ No newline at end of file + return merged_dict diff --git a/main.py b/main.py index a3a095c..e7ef0c0 100644 --- a/main.py +++ b/main.py @@ -1,23 +1,23 @@ import yaml - +from aiogram import Dispatcher +from aiogram.webhook.aiohttp_server import (SimpleRequestHandler, + setup_application) from aiohttp import web -from aiogram import Dispatcher -from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application - -from commands import currency, start, settings from bot import bot, db +from commands import currency, settings, start + +config = yaml.safe_load(open("../config.yaml", "r", encoding="utf-8")) -config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8')) async def on_startup(bot: bot) -> None: await db.connect() await db._create_table() await bot.set_webhook( f"{config['webhook']['base_url']}{config['webhook']['path']}", - secret_token=config['webhook']['secret_token'], - allowed_updates=['inline_query', 'message', 'callback_query'] - ) + secret_token=config["webhook"]["secret_token"], + allowed_updates=["inline_query", "message", "callback_query"], + ) async def on_shutdown(): @@ -36,16 +36,14 @@ def main() -> None: app = web.Application() webhook_requests_handler = SimpleRequestHandler( - dispatcher=dp, - bot=bot, - secret_token=config['webhook']['secret_token'] + dispatcher=dp, bot=bot, secret_token=config["webhook"]["secret_token"] ) - webhook_requests_handler.register(app, path=config['webhook']['path']) + webhook_requests_handler.register(app, path=config["webhook"]["path"]) setup_application(app, dp, bot=bot) - web.run_app(app, host='0.0.0.0', port=443) + web.run_app(app, host="0.0.0.0", port=443) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/utils/format_number.py b/utils/format_number.py index d9a2766..6458ec6 100644 --- a/utils/format_number.py +++ b/utils/format_number.py @@ -1,22 +1,22 @@ from decimal import Decimal + def format_number(number): number = Decimal(str(number)) integer_part = number // 1 fractional_part = number - integer_part - formatted_integer = '{:,.0f}'.format(integer_part).replace(',', ' ') + formatted_integer = "{:,.0f}".format(integer_part).replace(",", " ") if fractional_part == 0: return formatted_integer - fractional_str = f"{fractional_part:.30f}".split('.')[1] + fractional_str = f"{fractional_part:.30f}".split(".")[1] first_non_zero = next( - (i for i, char in enumerate(fractional_str) if char != '0'), - len(fractional_str) - ) - result_fractional = fractional_str[:first_non_zero + 3] - result_fractional = result_fractional.rstrip('0') + (i for i, char in enumerate(fractional_str) if char != "0"), len(fractional_str) + ) + result_fractional = fractional_str[: first_non_zero + 3] + result_fractional = result_fractional.rstrip("0") if not result_fractional: return formatted_integer diff --git a/utils/inline_query.py b/utils/inline_query.py index 9a50d37..5054052 100644 --- a/utils/inline_query.py +++ b/utils/inline_query.py @@ -2,8 +2,10 @@ import re from aiogram import types + def esc_md(text: str) -> str: - return re.sub(r'([_*\[\]()~`>#+\-=|{}.!\\])', r'\\\1', text) + return re.sub(r"([_*\[\]()~`>#+\-=|{}.!\\])", r"\\\1", text) + async def reply(result_id: str, args: list, query: types.InlineQuery) -> None: if not args: @@ -24,7 +26,7 @@ async def reply(result_id: str, args: list, query: types.InlineQuery) -> None: title=title, description=description, caption=esc_md(title), - parse_mode="MarkdownV2" + parse_mode="MarkdownV2", ) else: article = types.InlineQueryResultArticle(