Compare commits

..

10 commits

17 changed files with 277 additions and 430 deletions

12
bot.py
View file

@ -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)
)
token=config["telegram_token"],
default=DefaultBotProperties(parse_mode=ParseMode.HTML),
)

View file

@ -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,46 +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 lang, chart, chart_period '
'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]
@ -82,20 +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_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)

View file

@ -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)
@ -74,7 +62,7 @@ def build_options_keyboard(
buttons.append(
[
InlineKeyboardButton(
text=locale.get("back"),
text=locale["back"],
callback_data=back_callback,
)
]
@ -85,25 +73,23 @@ def build_options_keyboard(
def get_chart_toggle_keyboard(
chart_enabled: bool, locale: dict
) -> InlineKeyboardMarkup:
toggle_text = (
locale.get("chart_disable")
if chart_enabled
else locale.get("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"
text=locale["chart_period"],
callback_data="chart_period",
),
InlineKeyboardButton(
text=locale.get("chart_period"),
callback_data="chart_period",
text=locale["setting_backend"],
callback_data="setting_backend",
),
],
[
InlineKeyboardButton(
text=locale.get("back"),
text=locale["back"],
callback_data="back_to_settings",
),
],
@ -130,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:
@ -153,20 +138,18 @@ async def settings_handler(message: types.Message):
inline_keyboard=[
[
InlineKeyboardButton(
text=locale.get("setting_chart"),
text=locale["setting_chart"],
callback_data="setting_chart",
),
InlineKeyboardButton(
text=locale.get("setting_lang"),
text=locale["setting_lang"],
callback_data="setting_lang",
),
],
]
)
await message.answer(
locale.get("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")
@ -174,10 +157,9 @@ 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,
@ -187,15 +169,14 @@ async def show_language_menu(callback: CallbackQuery):
back_callback="back_to_settings",
)
await safe_edit_message_text(
callback, locale.get("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,
)
@ -209,13 +190,56 @@ 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))
@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["chart_backend"]
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, locale.get("choose_language"), keyboard
callback, f"{locale['choose_chart_backend']}", keyboard
)
await callback.answer(
locale.get("language_set").format(lang=lang)
@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["choose_chart_backend"], keyboard)
@router.callback_query(lambda c: c.data == "back_to_settings")
async def back_to_settings(callback: CallbackQuery):
@ -225,26 +249,30 @@ async def back_to_settings(callback: CallbackQuery):
inline_keyboard=[
[
InlineKeyboardButton(
text=locale.get("setting_chart"),
text=locale["setting_chart"],
callback_data="setting_chart",
),
InlineKeyboardButton(
text=locale.get("setting_lang"),
text=locale["setting_lang"],
callback_data="setting_lang",
),
InlineKeyboardButton(
text=locale["setting_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 +281,18 @@ async def show_chart_settings(callback: CallbackQuery):
chart_status = bool(data.get("chart", 1))
period = data.get("chart_period")
status_text = locale.get("enabled") \
if chart_status \
else locale.get("disabled")
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')}\n"
f"{locale.get('status')}: {status_text}\n"
f"{locale.get('period')}: {period_text}"
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)
@ -271,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)
@ -281,15 +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
)
await callback.answer(
locale.get(
f"chart_now_{'enabled' if new_status else 'disabled'}",
f"Chart now {'enabled' if new_status else 'disabled'}",
)
"UPDATE users SET chart = $1 WHERE user_id = $2",
new_status,
callback.from_user.id,
)
await show_chart_settings(callback)
@ -298,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)
@ -314,19 +337,16 @@ async def change_chart_period(callback: CallbackQuery):
back_callback="setting_chart",
)
await safe_edit_message_text(
callback,
locale.get("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)
@ -339,9 +359,4 @@ async def set_chart_period(callback: CallbackQuery):
back_callback="setting_chart",
)
await safe_edit_message_text(
callback,
locale.get("choose_period"),
keyboard
)
await callback.answer(locale.get("period_set").format(period=period))
await safe_edit_message_text(callback, locale["choose_period"], keyboard)

View file

@ -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")
]
])
await message.reply(
text,
parse_mode="MarkdownV2",
reply_markup=keyboard
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)

View file

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

View file

@ -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,7 +67,6 @@ 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.
@ -74,7 +75,7 @@ class Database:
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]:
"""

View file

@ -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():
@ -28,37 +29,37 @@ class Converter:
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=3)
) as session:
async with session.get(
f"{config['kekkai_instance']}/api/metadata"
) as res:
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
@ -67,28 +68,26 @@ class Converter:
timeout=aiohttp.ClientTimeout(total=3)
) as session:
async with session.get(
'https://duckduckgo.com/js/spice/currency/'
f'{self.amount}/{self.from_currency}/{self.conv_currency}'
"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)

View file

@ -1,26 +1,34 @@
import time
from http import HTTPStatus
from urllib.parse import urlencode
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) -> (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:
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(),
}
# 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}"
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

View file

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

View file

@ -40,3 +40,6 @@ week: "Неделя"
month: "Месяц"
quarter: "Квартал"
year: "Год"
setting_backend: "Бэкенд графиков"
choose_chart_backend: "Выберите бэкэнд для создания графиков"
selected_chart_backend: "Бэкенд для графиков"

View file

@ -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,4 +22,14 @@ 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

26
main.py
View file

@ -1,22 +1,22 @@
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"],
)
@ -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()

View file

@ -1,5 +0,0 @@
[mypy]
check_untyped_defs = True
warn_return_any = True
warn_unreachable = True
show_error_codes = True

193
pylintrc
View file

@ -1,193 +0,0 @@
[MAIN]
analyse-fallback-blocks=no
clear-cache-post-run=no
extension-pkg-allow-list=
extension-pkg-whitelist=
fail-on=
fail-under=10
ignore=CVS
ignore-paths=
ignore-patterns=^\.#
ignored-modules=
jobs=4
limit-inference-results=100
load-plugins=
persistent=yes
py-version=3.10
recursive=no
suggestion-mode=yes
unsafe-load-any-extension=no
[BASIC]
argument-naming-style=snake_case
attr-naming-style=snake_case
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
bad-names-rgxs=
class-attribute-naming-style=any
class-const-naming-style=UPPER_CASE
class-naming-style=PascalCase
const-naming-style=UPPER_CASE
docstring-min-length=-1
function-naming-style=snake_case
good-names=i,
j,
k,
ex,
Run,
_
good-names-rgxs=
include-naming-hint=no
inlinevar-naming-style=any
method-naming-style=snake_case
module-naming-style=snake_case
name-group=
no-docstring-rgx=^_
property-classes=abc.abstractproperty
variable-naming-style=snake_case
[CLASSES]
check-protected-access-in-special-methods=no
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
valid-classmethod-first-arg=cls
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
exclude-too-few-public-methods=
ignored-parents=
max-args=5
max-attributes=7
max-bool-expr=5
max-branches=12
max-locals=15
max-parents=7
max-public-methods=20
max-returns=6
max-statements=50
min-public-methods=0
[EXCEPTIONS]
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
expected-line-ending-format=
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
indent-after-paren=4
indent-string=' '
max-line-length=100
max-module-lines=1000
single-line-class-stmt=no
single-line-if-stmt=no
[IMPORTS]
allow-any-import-level=
allow-reexport-from-package=no
allow-wildcard-with-all=no
deprecated-modules=
ext-import-graph=
import-graph=
int-import-graph=
known-standard-library=
known-third-party=enchant
preferred-modules=
[LOGGING]
logging-format-style=old
logging-modules=logging
[MESSAGES CONTROL]
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
disable=raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
missing-module-docstring,
missing-class-docstring,
missing-function-docstring,
broad-exception-caught
enable=c-extension-no-member
[METHOD_ARGS]
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
notes=FIXME,
XXX,
TODO
notes-rgx=
[REFACTORING]
max-nested-blocks=5
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
msg-template=
reports=no
score=yes
[SIMILARITIES]
ignore-comments=yes
ignore-docstrings=yes
ignore-imports=yes
ignore-signatures=yes
min-similarity-lines=4
[SPELLING]
max-spelling-suggestions=4
spelling-dict=
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
spelling-ignore-words=
spelling-private-dict-file=
spelling-store-unknown-words=no
[STRING]
check-quote-consistency=no
check-str-concat-over-line-jumps=no
[TYPECHECK]
contextmanager-decorators=contextlib.contextmanager
generated-members=
ignore-none=yes
ignore-on-opaque-inference=yes
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
missing-member-hint=yes
missing-member-hint-distance=1
missing-member-max-choices=1
mixin-class-rgx=.*[Mm]ixin
signature-mutators=
[VARIABLES]
additional-builtins=
allow-global-unused-variables=yes
allowed-redefined-builtins=
callbacks=cb_,
_cb
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
ignored-argument-names=_.*|^ignored_|^unused_
init-import=no
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io

20
pyproject.toml Normal file
View file

@ -0,0 +1,20 @@
[tool.isort]
profile = "black"
line_length = 79
known_first_party = ["kekkai", "chart"]
[tool.black]
line-length = 79
target-version = ['py312']
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.mypy_cache
| \.venv
| build
| dist
| Dockerfile
| .*\.md$
)/
'''

View file

@ -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)
(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')
result_fractional = fractional_str[: first_non_zero + 3]
result_fractional = result_fractional.rstrip("0")
if not result_fractional:
return formatted_integer

View file

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