mirror of
https://github.com/Redume/Shirino.git
synced 2025-05-19 10:15:26 +01:00
Compare commits
23 commits
65236eeec8
...
3a83e55343
Author | SHA1 | Date | |
---|---|---|---|
3a83e55343 | |||
c0e9152f4f | |||
d3fcc06b68 | |||
ae695d6f48 | |||
f92e24816d | |||
6f415c2378 | |||
ff8d23330b | |||
8569a0a924 | |||
01b8d01285 | |||
5fe0a5ae3a | |||
722b79b523 | |||
6bf8b4c049 | |||
fd6f0b699e | |||
9b3ed607b0 | |||
1ae3ec9aec | |||
210c60552f | |||
757c74f779 | |||
b671981bb3 | |||
b5d8edcc38 | |||
b4eba633b9 | |||
4aa4742cd2 | |||
e170b5bff2 | |||
86085e2feb |
14 changed files with 875 additions and 143 deletions
13
bot.py
Normal file
13
bot.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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'))
|
||||||
|
bot = Bot(
|
||||||
|
token=config['telegram_token'],
|
||||||
|
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
||||||
|
)
|
117
commands/currency.py
Normal file
117
commands/currency.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from aiogram import types, Router
|
||||||
|
from aiogram.filters import Command
|
||||||
|
|
||||||
|
from bot import bot, db
|
||||||
|
from functions.convert import Converter
|
||||||
|
from functions.create_chart import create_chart
|
||||||
|
from utils.format_number import format_number
|
||||||
|
from utils.inline_query import reply
|
||||||
|
from i18n.localization import I18n
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
i18n = I18n()
|
||||||
|
|
||||||
|
|
||||||
|
@router.inline_query()
|
||||||
|
async def currency(query: types.InlineQuery) -> None:
|
||||||
|
text = query.query.lower()
|
||||||
|
args = text.split()
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
lang = data.get('lang')
|
||||||
|
locale = i18n.get_locale(lang)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
conv = Converter()
|
||||||
|
|
||||||
|
from_currency, conv_currency = '', ''
|
||||||
|
|
||||||
|
if len(args) == 3:
|
||||||
|
try:
|
||||||
|
conv.amount = float(args[0].replace(',', '.'))
|
||||||
|
if conv.amount < 0:
|
||||||
|
await reply(
|
||||||
|
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
|
||||||
|
)
|
||||||
|
return
|
||||||
|
from_currency = args[1]
|
||||||
|
conv_currency = args[2]
|
||||||
|
elif len(args) == 2:
|
||||||
|
from_currency = args[0]
|
||||||
|
conv_currency = args[1]
|
||||||
|
else:
|
||||||
|
await reply(
|
||||||
|
result_id,
|
||||||
|
[(locale["error_unknown_currency"], None, None)],
|
||||||
|
query,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
conv.from_currency = from_currency.upper()
|
||||||
|
conv.conv_currency = conv_currency.upper()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await conv.convert()
|
||||||
|
except RuntimeError:
|
||||||
|
await reply(
|
||||||
|
result_id,
|
||||||
|
[(locale["error_currency_rate"], None, None)],
|
||||||
|
query
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
chart = None
|
||||||
|
|
||||||
|
if bool(data.get('chart', 1)):
|
||||||
|
chart = await create_chart(
|
||||||
|
from_currency,
|
||||||
|
conv_currency,
|
||||||
|
data.get('chart_period', 'month')
|
||||||
|
)
|
||||||
|
|
||||||
|
message = (
|
||||||
|
f"{format_number(conv.amount)} {conv.from_currency} = "
|
||||||
|
f"{conv.conv_amount} {conv.conv_currency}"
|
||||||
|
)
|
||||||
|
results = [(message, None, None)]
|
||||||
|
|
||||||
|
if chart:
|
||||||
|
results.insert(
|
||||||
|
0,
|
||||||
|
(
|
||||||
|
message,
|
||||||
|
None,
|
||||||
|
chart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await reply(result_id, results, query)
|
347
commands/settings.py
Normal file
347
commands/settings.py
Normal file
|
@ -0,0 +1,347 @@
|
||||||
|
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"),
|
||||||
|
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")
|
||||||
|
if chart_enabled
|
||||||
|
else locale.get("chart_enable")
|
||||||
|
)
|
||||||
|
return InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=toggle_text, callback_data="chart_toggle"
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=locale.get("chart_period"),
|
||||||
|
callback_data="chart_period",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=locale.get("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"),
|
||||||
|
callback_data="setting_chart",
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=locale.get("setting_lang"),
|
||||||
|
callback_data="setting_lang",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
locale.get("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
|
||||||
|
)
|
||||||
|
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 == "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"),
|
||||||
|
callback_data="setting_chart",
|
||||||
|
),
|
||||||
|
InlineKeyboardButton(
|
||||||
|
text=locale.get("setting_lang"),
|
||||||
|
callback_data="setting_lang",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
await safe_edit_message_text(
|
||||||
|
callback, locale.get("settings_title"), 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',
|
||||||
|
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") \
|
||||||
|
if chart_status \
|
||||||
|
else locale.get("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}"
|
||||||
|
)
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
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"),
|
||||||
|
keyboard
|
||||||
|
)
|
||||||
|
await callback.answer(locale.get("period_set").format(period=period))
|
42
commands/start.py
Normal file
42
commands/start.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
from aiogram import types, Router
|
||||||
|
from aiogram.filters import CommandStart
|
||||||
|
import re
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
|
||||||
|
locale = i18n.get_locale(data.get('lang'))
|
||||||
|
|
||||||
|
raw_template = locale.get("start_message")
|
||||||
|
raw_text = raw_template.format(bot_username=get_bot.username)
|
||||||
|
text = escape_md_v2(raw_text)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
6
database/schemas/data.sql
Normal file
6
database/schemas/data.sql
Normal file
|
@ -0,0 +1,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'
|
||||||
|
);
|
189
database/server.py
Normal file
189
database/server.py
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import json
|
||||||
|
from datetime import date, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
import aiosqlite
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8'))
|
||||||
|
|
||||||
|
def custom_encoder(obj):
|
||||||
|
"""
|
||||||
|
Custom JSON encoder for objects not serializable by default.
|
||||||
|
|
||||||
|
Converts date and datetime objects to ISO 8601 string format.
|
||||||
|
Raises TypeError for unsupported types.
|
||||||
|
"""
|
||||||
|
if isinstance(obj, (date, datetime)):
|
||||||
|
return obj.isoformat()
|
||||||
|
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
"""
|
||||||
|
Asynchronous SQLite database handler using aiosqlite.
|
||||||
|
|
||||||
|
This class manages a single asynchronous connection to an SQLite database file
|
||||||
|
and provides common methods for executing queries, including:
|
||||||
|
|
||||||
|
- Connecting and disconnecting to the database.
|
||||||
|
- Fetching a single row or multiple rows.
|
||||||
|
- Inserting data and returning the last inserted row ID.
|
||||||
|
- Updating or deleting data and returning the count of affected rows.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
db_path (str): Path to the SQLite database file.
|
||||||
|
conn (Optional[aiosqlite.Connection]): The active database connection, or None if disconnected.
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
connect(): Asynchronously open a connection to the database.
|
||||||
|
disconnect(): Asynchronously close the database connection.
|
||||||
|
fetch(query, *args): Execute a query and return a single row as a dictionary.
|
||||||
|
fetchmany(query, *args): Execute a query and return multiple rows as a list of dictionaries.
|
||||||
|
insert(query, *args): Execute an INSERT query and return the last inserted row ID.
|
||||||
|
update(query, *args): Execute an UPDATE or DELETE query and return the number of affected rows.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If any method is called before the database connection is established.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
db = Database("example.db")
|
||||||
|
await db.connect()
|
||||||
|
user = await db.fetch("SELECT * FROM users WHERE id = ?", user_id)
|
||||||
|
await db.disconnect()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db_path: str):
|
||||||
|
"""
|
||||||
|
Initialize Database instance.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db_path (str): Path to SQLite database file.
|
||||||
|
"""
|
||||||
|
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'
|
||||||
|
and executes them as a script.
|
||||||
|
"""
|
||||||
|
sql_file = Path(__file__).parent / "schemas" / "data.sql"
|
||||||
|
sql = sql_file.read_text(encoding='utf-8')
|
||||||
|
|
||||||
|
async with self.conn.execute("BEGIN"):
|
||||||
|
await self.conn.executescript(sql)
|
||||||
|
await self.conn.commit()
|
||||||
|
|
||||||
|
async def connect(self) -> None:
|
||||||
|
"""
|
||||||
|
Open SQLite database connection asynchronously.
|
||||||
|
"""
|
||||||
|
self.conn = await aiosqlite.connect(self.db_path)
|
||||||
|
self.conn.row_factory = aiosqlite.Row # return dict-like rows
|
||||||
|
|
||||||
|
async def disconnect(self) -> None:
|
||||||
|
"""
|
||||||
|
Close SQLite database connection asynchronously.
|
||||||
|
"""
|
||||||
|
if self.conn:
|
||||||
|
await self.conn.close()
|
||||||
|
self.conn = None
|
||||||
|
|
||||||
|
async def fetch(self, query: str, *args) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute a query and fetch a single row as a dictionary.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): SQL query string.
|
||||||
|
*args: Parameters for the SQL query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: The first row returned by the query as a dict,
|
||||||
|
or an empty dict if no row is found.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the database connection is not initialized.
|
||||||
|
"""
|
||||||
|
if not self.conn:
|
||||||
|
raise RuntimeError("Database connection is not initialized.")
|
||||||
|
|
||||||
|
async with self.conn.execute(query, args) as cursor:
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return json.loads(json.dumps(dict(row), default=custom_encoder))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def fetchmany(self, query: str, *args) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Execute a query and fetch multiple rows as a list of dictionaries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): SQL query string.
|
||||||
|
*args: Parameters for the SQL query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Dict[str, Any]]: List of rows as dictionaries,
|
||||||
|
or empty list if no rows found.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the database connection is not initialized.
|
||||||
|
"""
|
||||||
|
if not self.conn:
|
||||||
|
raise RuntimeError("Database connection is not initialized.")
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
) if rows else []
|
||||||
|
|
||||||
|
async def insert(self, query: str, *args) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute an INSERT query and return the last inserted row ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): SQL INSERT query string.
|
||||||
|
*args: Parameters for the SQL query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Dictionary containing the last inserted row ID.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the database connection is not initialized.
|
||||||
|
"""
|
||||||
|
if not self.conn:
|
||||||
|
raise RuntimeError("Database connection is not initialized.")
|
||||||
|
|
||||||
|
async with self.conn.execute(query, args) as cursor:
|
||||||
|
await self.conn.commit()
|
||||||
|
return {"last_row_id": cursor.lastrowid}
|
||||||
|
|
||||||
|
async def update(self, query: str, *args) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Execute an UPDATE or DELETE query and return affected rows count.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (str): SQL UPDATE or DELETE query string.
|
||||||
|
*args: Parameters for the SQL query.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Dictionary containing the number of affected rows.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the database connection is not initialized.
|
||||||
|
"""
|
||||||
|
if not self.conn:
|
||||||
|
raise RuntimeError("Database connection is not initialized.")
|
||||||
|
|
||||||
|
async with self.conn.execute(query, args) as cursor:
|
||||||
|
await self.conn.commit()
|
||||||
|
return {"rows_affected": cursor.rowcount}
|
|
@ -5,6 +5,7 @@ services:
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- './config.yaml:/config.yaml'
|
- './config.yaml:/config.yaml'
|
||||||
|
- 'shirino-db:/data'
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:latest
|
image: nginx:latest
|
||||||
|
@ -18,3 +19,4 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
shirino:
|
shirino:
|
||||||
driver: local
|
driver: local
|
||||||
|
shirino-db:
|
|
@ -5,10 +5,16 @@ import aiohttp
|
||||||
|
|
||||||
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) -> (dict, None):
|
async def create_chart(
|
||||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session:
|
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(
|
async with session.get(
|
||||||
f'{config["kekkai_instance"]}/api/getChart/month/', params={
|
f'{config["kekkai_instance"]}/api/getChart/{period}/',
|
||||||
|
params={
|
||||||
'from_currency': from_currency,
|
'from_currency': from_currency,
|
||||||
'conv_currency': conv_currency
|
'conv_currency': conv_currency
|
||||||
}) as res:
|
}) as res:
|
||||||
|
|
42
i18n/locales/en.yaml
Normal file
42
i18n/locales/en.yaml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# /commands/start.py
|
||||||
|
start_message: |
|
||||||
|
Shirino is a telegram bot for converting fiat or cryptocurrency.
|
||||||
|
The example of use occurs via inline query:
|
||||||
|
`@{bot_username} USD RUB`
|
||||||
|
`@{bot_username} 12 USD RUB`
|
||||||
|
source_code_button: "Source Code"
|
||||||
|
|
||||||
|
# /commands/currency.py
|
||||||
|
currency_example: |
|
||||||
|
@{bot_username} USD RUB
|
||||||
|
@{bot_username} 12 USD RUB
|
||||||
|
error_not_enough_args: 2 or 3 arguments are required.
|
||||||
|
error_invalid_number: "Please enter a valid number for the amount."
|
||||||
|
error_negative_amount: "Negative amounts are not supported."
|
||||||
|
error_currency_rate: "The currency exchange rate could not be determined."
|
||||||
|
error_unknown_currency: "The source and target currency could not be determined."
|
||||||
|
chart: "Chart"
|
||||||
|
|
||||||
|
# /commands/settings.py
|
||||||
|
settings_title: "Bot Settings"
|
||||||
|
setting_chart: "Chart Settings"
|
||||||
|
setting_lang: "Change Language"
|
||||||
|
back: "Back"
|
||||||
|
choose_language: "Choose language:"
|
||||||
|
language_set: "Language set: {lang}"
|
||||||
|
chart_settings: "Chart Settings"
|
||||||
|
status: "Status"
|
||||||
|
period: "Period"
|
||||||
|
enabled: "Enabled"
|
||||||
|
disabled: "Disabled"
|
||||||
|
chart_disable: "Disable Chart"
|
||||||
|
chart_enable: "Enable Chart"
|
||||||
|
chart_period: "Chart Period"
|
||||||
|
chart_now_enabled: "Chart is now enabled"
|
||||||
|
chart_now_disabled: "Chart is now disabled"
|
||||||
|
choose_period: "Choose chart period:"
|
||||||
|
period_set: "Chart period set to: {period}"
|
||||||
|
week: "Week"
|
||||||
|
month: "Month"
|
||||||
|
quarter: "Quarter"
|
||||||
|
year: "Year"
|
42
i18n/locales/ru.yaml
Normal file
42
i18n/locales/ru.yaml
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# /commads/start.py
|
||||||
|
start_message: |
|
||||||
|
Shirino — Telegram-бот для конвертации фиатных и криптовалют.
|
||||||
|
Пример использования через inline-запрос:
|
||||||
|
`@{bot_username} USD RUB`
|
||||||
|
`@{bot_username} 12 USD RUB`
|
||||||
|
source_code_button: "Исходный код"
|
||||||
|
|
||||||
|
# /commands/currency.py
|
||||||
|
currency_example: |
|
||||||
|
@{bot_username} USD RUB
|
||||||
|
@{bot_username} 12 USD RUB
|
||||||
|
error_not_enough_args: Необходимо ввести 2 или 3 аргумента.
|
||||||
|
error_invalid_number: "Пожалуйста, введите правильное число для суммы."
|
||||||
|
error_negative_amount: "Отрицательные суммы не поддерживаются."
|
||||||
|
error_currency_rate: "Не удалось определить курс обмена валют."
|
||||||
|
error_unknown_currency: "Не удалось определить исходную и целевую валюту."
|
||||||
|
chart: "График"
|
||||||
|
|
||||||
|
# /commands/settings.py
|
||||||
|
settings_title: "Настройки бота"
|
||||||
|
setting_chart: "Настройка графиков"
|
||||||
|
setting_lang: "Смена языка"
|
||||||
|
back: "Назад"
|
||||||
|
choose_language: "Выберите язык:"
|
||||||
|
language_set: "Язык установлен: {lang}"
|
||||||
|
chart_settings: "Настройки графика"
|
||||||
|
status: "Статус"
|
||||||
|
period: "Период"
|
||||||
|
enabled: "Включён"
|
||||||
|
disabled: "Выключен"
|
||||||
|
chart_disable: "Выключить график"
|
||||||
|
chart_enable: "Включить график"
|
||||||
|
chart_period: "Период графика"
|
||||||
|
chart_now_enabled: "График теперь включён"
|
||||||
|
chart_now_disabled: "График теперь выключен"
|
||||||
|
choose_period: "Выберите период графика:"
|
||||||
|
period_set: "Период графика установлен: {period}"
|
||||||
|
week: "Неделя"
|
||||||
|
month: "Месяц"
|
||||||
|
quarter: "Квартал"
|
||||||
|
year: "Год"
|
24
i18n/localization.py
Normal file
24
i18n/localization.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class I18n:
|
||||||
|
"""Load every YAML file in i18n/locales and let you pull out a single-language dict."""
|
||||||
|
|
||||||
|
def __init__(self, locales_dir: str = "i18n/locales", default_lang: str = "en"):
|
||||||
|
base_path = Path(__file__).parent.parent
|
||||||
|
self.locales_dir = base_path / locales_dir
|
||||||
|
self.default_lang = default_lang
|
||||||
|
self.translations: dict[str, dict] = {}
|
||||||
|
self._load_translations()
|
||||||
|
|
||||||
|
def _load_translations(self) -> None:
|
||||||
|
for file in self.locales_dir.glob("*.yaml"):
|
||||||
|
lang = file.stem.lower()
|
||||||
|
with file.open(encoding="utf-8") as f:
|
||||||
|
self.translations[lang] = yaml.safe_load(f)
|
||||||
|
|
||||||
|
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])
|
138
main.py
138
main.py
|
@ -1,144 +1,38 @@
|
||||||
import hashlib
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
|
|
||||||
from aiogram import Bot, Dispatcher, Router, types
|
from aiogram import Dispatcher
|
||||||
from aiogram.client.default import DefaultBotProperties
|
|
||||||
from aiogram.enums import ParseMode
|
|
||||||
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
||||||
from aiogram.filters import CommandStart
|
|
||||||
|
|
||||||
from functions.convert import Converter
|
from commands import currency, start, settings
|
||||||
from functions.create_chart import create_chart
|
from bot import bot, db
|
||||||
from utils.format_number import format_number
|
|
||||||
from utils.inline_query import reply
|
|
||||||
|
|
||||||
config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8'))
|
config = yaml.safe_load(open('../config.yaml', 'r', encoding='utf-8'))
|
||||||
bot = Bot(
|
|
||||||
token=config['telegram_token'],
|
|
||||||
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
|
|
||||||
)
|
|
||||||
|
|
||||||
router = Router()
|
async def on_startup(bot: bot) -> None:
|
||||||
|
await db.connect()
|
||||||
@router.message(CommandStart())
|
await db._create_table()
|
||||||
async def start(message: types.Message):
|
|
||||||
get_bot = await bot.get_me()
|
|
||||||
await message.reply(
|
|
||||||
'Shirino is a telegram bot for converting fiat or cryptocurrency. '
|
|
||||||
'The example of use occurs via inline query:\n'
|
|
||||||
f'`@{get_bot.username} USD RUB` \n'
|
|
||||||
f'`@{get_bot.username} 12 USD RUB` \n\n'
|
|
||||||
'[Source Code](https://github.com/Redume/Shirino)',
|
|
||||||
parse_mode='markdown'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.inline_query()
|
|
||||||
async def currency(query: types.InlineQuery) -> None:
|
|
||||||
text = query.query.lower()
|
|
||||||
args = text.split()
|
|
||||||
result_id = hashlib.md5(text.encode()).hexdigest()
|
|
||||||
|
|
||||||
get_bot = await bot.get_me()
|
|
||||||
|
|
||||||
if len(args) < 2:
|
|
||||||
return await reply(result_id,
|
|
||||||
[("2 or 3 arguments are required.",
|
|
||||||
f'@{get_bot.username} USD RUB \n'
|
|
||||||
f'@{get_bot.username} 12 USD RUB',
|
|
||||||
None, None)],
|
|
||||||
query)
|
|
||||||
|
|
||||||
conv = Converter()
|
|
||||||
|
|
||||||
from_currency, conv_currency = '', ''
|
|
||||||
|
|
||||||
if len(args) == 3:
|
|
||||||
try:
|
|
||||||
conv.amount = float(args[0].replace(',', '.'))
|
|
||||||
if conv.amount < 0:
|
|
||||||
return await reply(
|
|
||||||
result_id,
|
|
||||||
[
|
|
||||||
("Negative amounts are not supported.", None, None)
|
|
||||||
],
|
|
||||||
query
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
return await reply(
|
|
||||||
result_id,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
"Please enter a valid number for the amount.",
|
|
||||||
f'@{get_bot.username} USD RUB \n'
|
|
||||||
f'@{get_bot.username} 12 USD RUB',
|
|
||||||
None, None
|
|
||||||
)
|
|
||||||
],
|
|
||||||
query)
|
|
||||||
|
|
||||||
from_currency = args[1]
|
|
||||||
conv_currency = args[2]
|
|
||||||
elif len(args) == 2:
|
|
||||||
from_currency = args[0]
|
|
||||||
conv_currency = args[1]
|
|
||||||
else:
|
|
||||||
return await reply(
|
|
||||||
result_id,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
'The source and target currency could not be determined.',
|
|
||||||
None, None
|
|
||||||
)
|
|
||||||
],
|
|
||||||
query
|
|
||||||
)
|
|
||||||
|
|
||||||
conv.from_currency = from_currency.upper()
|
|
||||||
conv.conv_currency = conv_currency.upper()
|
|
||||||
try:
|
|
||||||
await conv.convert()
|
|
||||||
except RuntimeError:
|
|
||||||
return await reply(
|
|
||||||
result_id,
|
|
||||||
[
|
|
||||||
(
|
|
||||||
'The currency exchange rate could not be determined',
|
|
||||||
None, None
|
|
||||||
)
|
|
||||||
],
|
|
||||||
query
|
|
||||||
)
|
|
||||||
|
|
||||||
chart = await create_chart(from_currency, conv_currency)
|
|
||||||
|
|
||||||
message = f'{format_number(conv.amount)} {conv.from_currency} ' \
|
|
||||||
f'= {conv.conv_amount} {conv.conv_currency}'
|
|
||||||
|
|
||||||
results = [(message, None, None)]
|
|
||||||
|
|
||||||
if chart:
|
|
||||||
results.insert(0, (f'{message}\n[Chart]({chart})', None, chart))
|
|
||||||
|
|
||||||
await reply(result_id, results, query)
|
|
||||||
|
|
||||||
|
|
||||||
async def on_startup(bot: Bot) -> None:
|
|
||||||
await bot.set_webhook(
|
await bot.set_webhook(
|
||||||
f"{config['webhook']['base_url']}{config['webhook']['path']}",
|
f"{config['webhook']['base_url']}{config['webhook']['path']}",
|
||||||
secret_token=config['webhook']['secret_token'],
|
secret_token=config['webhook']['secret_token'],
|
||||||
allowed_updates=['inline_query', 'message']
|
allowed_updates=['inline_query', 'message', 'callback_query']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def on_shutdown():
|
||||||
|
await db.disconnect()
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
dp = Dispatcher()
|
dp = Dispatcher()
|
||||||
|
|
||||||
dp.include_router(router)
|
dp.include_router(currency.router)
|
||||||
|
dp.include_router(start.router)
|
||||||
|
dp.include_router(settings.router)
|
||||||
|
|
||||||
dp.startup.register(on_startup)
|
dp.startup.register(on_startup)
|
||||||
|
dp.shutdown.register(on_shutdown)
|
||||||
|
|
||||||
app = web.Application()
|
app = web.Application()
|
||||||
webhook_requests_handler = SimpleRequestHandler(
|
webhook_requests_handler = SimpleRequestHandler(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
aiohttp~=3.9.5
|
aiohttp~=3.9.5
|
||||||
PyYAML~=6.0.1
|
PyYAML~=6.0.1
|
||||||
aiogram~=3.15.0
|
aiogram~=3.15.0
|
||||||
|
aiosqlite~=0.21.0
|
|
@ -2,6 +2,9 @@ import re
|
||||||
|
|
||||||
from aiogram import types
|
from aiogram import types
|
||||||
|
|
||||||
|
def esc_md(text: str) -> str:
|
||||||
|
return re.sub(r'([_*\[\]()~`>#+\-=|{}.!\\])', r'\\\1', text)
|
||||||
|
|
||||||
async def reply(result_id: str, args: list, query: types.InlineQuery) -> None:
|
async def reply(result_id: str, args: list, query: types.InlineQuery) -> None:
|
||||||
if not args:
|
if not args:
|
||||||
return
|
return
|
||||||
|
@ -13,23 +16,27 @@ async def reply(result_id: str, args: list, query: types.InlineQuery) -> None:
|
||||||
description = arg[1] if arg[1] else None
|
description = arg[1] if arg[1] else None
|
||||||
img = arg[2] if arg[2] else None
|
img = arg[2] if arg[2] else None
|
||||||
|
|
||||||
|
if img:
|
||||||
|
article = types.InlineQueryResultPhoto(
|
||||||
|
id=f"{result_id}_{idx}",
|
||||||
|
photo_url=img,
|
||||||
|
thumbnail_url=img,
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
caption=esc_md(title),
|
||||||
|
parse_mode="MarkdownV2"
|
||||||
|
)
|
||||||
|
else:
|
||||||
article = types.InlineQueryResultArticle(
|
article = types.InlineQueryResultArticle(
|
||||||
id=f"{result_id}_{idx}",
|
id=f"{result_id}_{idx}",
|
||||||
title=re.sub(r'\bChart\b|\[([^\]]+)\]\([^)]+\)', '', title, flags=re.IGNORECASE),
|
title=title,
|
||||||
thumbnail_url=img,
|
|
||||||
description=description,
|
description=description,
|
||||||
input_message_content=types.InputTextMessageContent(
|
input_message_content=types.InputTextMessageContent(
|
||||||
message_text=title,
|
message_text=esc_md(title),
|
||||||
parse_mode='markdown'
|
parse_mode="MarkdownV2",
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
articles.append(article)
|
articles.append(article)
|
||||||
|
|
||||||
await query.answer(
|
await query.answer(results=articles, cache_time=0, is_personal=True)
|
||||||
results=articles,
|
|
||||||
parse_mode='markdown',
|
|
||||||
cache_time=0,
|
|
||||||
is_personal=True
|
|
||||||
)
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue