Compare commits

...

3 commits

6 changed files with 98 additions and 43 deletions

View file

@ -4,12 +4,13 @@ from datetime import datetime
from decimal import Decimal from decimal import Decimal
from http import HTTPStatus from http import HTTPStatus
import pytz
import aiohttp import aiohttp
import yaml import yaml
from utils.format_number import format_number from utils.format_number import format_number
config = yaml.safe_load(open('config.yaml')) config = yaml.safe_load(open('config.yaml', 'r', encoding='utf-8'))
class Converter: class Converter:
def __init__(self): def __init__(self):
@ -17,16 +18,30 @@ class Converter:
self.conv_amount: float = 0.0 self.conv_amount: float = 0.0
self.from_currency: str = '' self.from_currency: str = ''
self.conv_currency: str = '' self.conv_currency: str = ''
async def convert(self) -> None: async def convert(self) -> None:
if not await self.kekkai(): if not await self.kekkai():
await self.ddg() await self.ddg()
self.conv_amount = format_number(Decimal(self.conv_amount)) self.conv_amount = format_number(Decimal(self.conv_amount))
async def get_timezone(self) -> str:
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=3)
) as session:
async with session.get(
f'{config['kekkai_instance']}/api/metadata'
) as res:
if not HTTPStatus(res.status).is_success:
return 'UTC'
data = await res.json()
return data.get('timezone', 'UTC')
async def kekkai(self) -> bool: async def kekkai(self) -> bool:
date = datetime.today().strftime('%Y-%m-%d') timezone = pytz.timezone(await self.get_timezone())
date = datetime.now(timezone).strftime('%Y-%m-%d')
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session:
async with session.get(f'{config['kekkai_instance']}/api/getRate/', params={ async with session.get(f'{config['kekkai_instance']}/api/getRate/', params={
@ -45,26 +60,32 @@ class Converter:
async def ddg(self) -> None: async def ddg(self) -> None:
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session: async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=3)
) as session:
async with session.get( async with session.get(
'https://duckduckgo.com/js/spice/currency/' 'https://duckduckgo.com/js/spice/currency/'
f'{self.amount}/{self.from_currency}/{self.conv_currency}' f'{self.amount}/{self.from_currency}/{self.conv_currency}'
) as res: ) as res:
data_text = await res.text() 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) data.pop(key, None)
if not data.get('to'): if not data.get('to'):
raise RuntimeError('Failed to get the exchange rate from DuckDuckGo') raise RuntimeError(
'Failed to get the exchange rate from DuckDuckGo'
)
conv = data.get('to')[0] conv = data.get('to')[0]
conv_amount = conv.get('mid') conv_amount = conv.get('mid')
if conv_amount is None: 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) self.conv_amount = float(conv_amount)

View file

@ -1,19 +1,20 @@
from http import HTTPStatus
import yaml import yaml
import aiohttp import aiohttp
from http import HTTPStatus config = yaml.safe_load(open('config.yaml', 'r', encoding='utf-8'))
config = yaml.safe_load(open('config.yaml'))
async def create_chart(from_currency: str, conv_currency: str) -> (dict, None): async def create_chart(from_currency: str, conv_currency: str) -> (dict, None):
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session: async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session:
async with session.get(f'{config["kekkai_instance"]}/api/getChart/month/', params={ async with session.get(
'from_currency': from_currency, f'{config["kekkai_instance"]}/api/getChart/month/', params={
'conv_currency': conv_currency 'from_currency': from_currency,
}) as res: 'conv_currency': conv_currency
}) as res:
if not HTTPStatus(res.status).is_success: if not HTTPStatus(res.status).is_success:
return None return None
data = await res.json() data = await res.json()
return data.get('message', None) return data.get('message', None)

75
main.py
View file

@ -1,8 +1,4 @@
from functions.convert import Converter import hashlib
from utils.format_number import format_number
from utils.inline_query import reply
from functions.create_chart import create_chart
import yaml import yaml
from aiohttp import web from aiohttp import web
@ -13,10 +9,16 @@ 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 aiogram.filters import CommandStart
import hashlib 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
config = yaml.safe_load(open('config.yaml')) config = yaml.safe_load(open('config.yaml', 'r', encoding='utf-8'))
bot = Bot(token=config['telegram_token'], default=DefaultBotProperties(parse_mode=ParseMode.HTML)) bot = Bot(
token=config['telegram_token'],
default=DefaultBotProperties(parse_mode=ParseMode.HTML)
)
router = Router() router = Router()
@ -57,13 +59,26 @@ async def currency(query: types.InlineQuery) -> None:
try: try:
conv.amount = float(args[0].replace(',', '.')) conv.amount = float(args[0].replace(',', '.'))
if conv.amount < 0: if conv.amount < 0:
return await reply(result_id, [("Negative amounts are not supported.", None, None)], query) return await reply(
result_id,
[
("Negative amounts are not supported.", None, None)
],
query
)
except ValueError: except ValueError:
return await reply(result_id, [("Please enter a valid number for the amount.", return await reply(
f'@{get_bot.username} USD RUB \n' result_id,
f'@{get_bot.username} 12 USD RUB', [
None, None)], query) (
"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] from_currency = args[1]
conv_currency = args[2] conv_currency = args[2]
@ -71,23 +86,37 @@ async def currency(query: types.InlineQuery) -> None:
from_currency = args[0] from_currency = args[0]
conv_currency = args[1] conv_currency = args[1]
else: else:
return await reply(result_id, return await reply(
[( result_id,
'The source and target currency could not be determined.', [
None, None (
)], 'The source and target currency could not be determined.',
query) None, None
)
],
query
)
conv.from_currency = from_currency.upper() conv.from_currency = from_currency.upper()
conv.conv_currency = conv_currency.upper() conv.conv_currency = conv_currency.upper()
try: try:
await conv.convert() await conv.convert()
except RuntimeError as e: except RuntimeError:
return await reply(result_id, [('The currency exchange rate could not be determined', None, None)], query) return await reply(
result_id,
[
(
'The currency exchange rate could not be determined',
None, None
)
],
query
)
chart = await create_chart(from_currency, conv_currency) chart = await create_chart(from_currency, conv_currency)
message = f'{format_number(conv.amount)} {conv.from_currency} = {conv.conv_amount} {conv.conv_currency}' message = f'{format_number(conv.amount)} {conv.from_currency} ' \
f'= {conv.conv_amount} {conv.conv_currency}'
results = [(message, None, None)] results = [(message, None, None)]
@ -99,7 +128,7 @@ async def currency(query: types.InlineQuery) -> None:
async def on_startup(bot: Bot) -> None: 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']
) )

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
pytz~=2025.2

View file

@ -11,7 +11,10 @@ def format_number(number):
return formatted_integer 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)) first_non_zero = next(
(i for i, char in enumerate(fractional_str) if char != '0'),
len(fractional_str)
)
result_fractional = fractional_str[:first_non_zero + 3] result_fractional = fractional_str[:first_non_zero + 3]
result_fractional = result_fractional.rstrip('0') result_fractional = result_fractional.rstrip('0')

View file

@ -1,7 +1,7 @@
from aiogram import types
import re import re
from aiogram import types
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
@ -32,4 +32,4 @@ async def reply(result_id: str, args: list, query: types.InlineQuery) -> None:
parse_mode='markdown', parse_mode='markdown',
cache_time=0, cache_time=0,
is_personal=True is_personal=True
) )