Compare commits

...

23 commits

Author SHA1 Message Date
3a83e55343 chore: change status gen charts if users not exists db
Some checks failed
Create and publish a Docker image / build-and-push-image (push) Has been cancelled
2025-05-18 11:24:38 +03:00
c0e9152f4f chore: delete garbadge line 2025-05-17 18:24:45 +03:00
d3fcc06b68 fix: fix position lang 2025-05-17 18:22:46 +03:00
ae695d6f48 chore: optimization code 2025-05-17 12:34:10 +03:00
f92e24816d chore: wrap lines to 79 chars for PEP8 compliance and improved readability, no logic changes 2025-05-17 12:29:50 +03:00
6f415c2378 fix: fixed a bug with the message not changing 2025-05-17 12:26:29 +03:00
ff8d23330b chore: added localization files 2025-05-17 11:50:26 +03:00
8569a0a924 feat: bot customization 2025-05-17 11:50:08 +03:00
01b8d01285 chore: made localization in the inline command to get the course of the command 2025-05-17 11:49:57 +03:00
5fe0a5ae3a chore: added a new field for period customization 2025-05-17 00:35:27 +03:00
722b79b523 chore: Added translation for the start command, code link made as a button 2025-05-17 00:34:56 +03:00
6bf8b4c049 chore: a class is written to get and work with localization 2025-05-17 00:34:28 +03:00
fd6f0b699e chore: charts are now as a picture natively, not a link 2025-05-17 00:34:05 +03:00
9b3ed607b0 chore: fix import 2025-05-15 15:06:53 +03:00
1ae3ec9aec chore: the link in the post has been replaced with a button 2025-05-15 15:06:40 +03:00
210c60552f chore: fix import 2025-05-15 15:03:21 +03:00
757c74f779 chore: the database is now not deleted when the container is stopped 2025-05-15 14:55:08 +03:00
b671981bb3 chore: added new updates and moved the database instance to another file 2025-05-15 14:54:39 +03:00
b5d8edcc38 chore: added a new field to customize the schedule period 2025-05-15 14:53:52 +03:00
b4eba633b9 chore: the commands have been organized into files 2025-05-15 10:53:55 +03:00
4aa4742cd2 feat: add sqlite database 2025-05-15 01:06:03 +03:00
e170b5bff2 chore: set return type is none for start command 2025-05-15 00:34:37 +03:00
86085e2feb chore: made line breaks 2025-04-26 14:04:27 +03:00
14 changed files with 875 additions and 143 deletions

13
bot.py Normal file
View 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
View 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
View 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
View 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
)

View 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
View 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}

View file

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

View file

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

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

View file

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

View file

@ -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.InlineQueryResultArticle( article = types.InlineQueryResultPhoto(
id=f"{result_id}_{idx}", id=f"{result_id}_{idx}",
title=re.sub(r'\bChart\b|\[([^\]]+)\]\([^)]+\)', '', title, flags=re.IGNORECASE), photo_url=img,
thumbnail_url=img, thumbnail_url=img,
description=description, title=title,
input_message_content=types.InputTextMessageContent( description=description,
message_text=title, caption=esc_md(title),
parse_mode='markdown' parse_mode="MarkdownV2"
)
else:
article = types.InlineQueryResultArticle(
id=f"{result_id}_{idx}",
title=title,
description=description,
input_message_content=types.InputTextMessageContent(
message_text=esc_md(title),
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
)