Compare commits

..

No commits in common. "019f844e4ffd431ff7879028568147ddca132116" and "c2d675aa09a44a1c441059879f999cd9fd8454e2" have entirely different histories.

6 changed files with 130 additions and 169 deletions

View file

@ -1,4 +1,4 @@
Copyright (c) 2023-2024 Redume, DarkCat09 (Chechkenev Andrey) Copyright (c) 2023 Redume, DarkCat09 (Chechkenev Andrey)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,5 +1,6 @@
debug: false # debug logging debug: false # debug logging
timeout: 2 # http requests timeout timeout: 2 # http requests timeout
ndigits: 3 # digits after floating point or after zeroes
coinapi_keys: coinapi_keys:
- key - key
- key2 # coinapi keys list - key2 # coinapi keys list

View file

@ -1,4 +0,0 @@
{
"RUB": ["руб", "рубли", "рубля", "рублей", "рубль", "rub", "rouble", "roubles"],
"USD": ["доллары", "доллар", "доллара", "зеленых", "usd", "dollar", "dollars"]
}

View file

@ -1,85 +0,0 @@
import yaml
import requests
import json
import re
from decimal import Decimal, ROUND_DOWN
from function.format_number import format_number
config = yaml.safe_load(open('config.yaml'))
coinapi_len = len(config['coinapi_keys'])
coinapi_active = [0]
class Converter:
def __init__(self):
self.amount: float = 1.0
self.conv_amount: float = 0.0
self.from_currency: str = ''
self.conv_currency: str = ''
def convert(self) -> None:
if not self.ddg():
self.coinapi()
number = Decimal(str(self.conv_amount))
self.conv_amount = format_number(number.quantize(Decimal('1.00'), rounding=ROUND_DOWN))
def ddg(self) -> bool:
res = requests.get('https://duckduckgo.com/js/spice/currency'
f'/{self.amount}'
f'/{self.from_currency}'
f'/{self.conv_currency}')
data = json.loads(re.findall(r'\(\s*(.*)\s*\);$', res.text)[0])
del data['terms']
del data['privacy']
del data['timestamp']
if len(data.get('to')) == 0:
return False
conv = data.get('to')[0]
conv_amount = conv.get('mid')
if conv_amount is None:
raise RuntimeError('Error when converting currency via DuckDuckGo')
self.conv_amount = float(conv_amount)
return True
def coinapi(self, depth: int = config['coinapi_keys']) -> None:
if depth <= 0:
raise RecursionError('Rate limit on all tokens')
resp = requests.get(
(
'https://rest.coinapi.io/v1/exchangerate'
f'/{self.from_currency}'
f'/{self.conv_currency}'
),
headers={
'X-CoinAPI-Key': config['coinapi_keys'][coinapi_active[0]],
},
timeout=config['timeout'],
)
if resp.status_code == 429:
rotate_token(config['coinapi_keys'], coinapi_active)
self.coinapi(depth - 1)
data = resp.json()
rate = data.get('rate')
if rate is None:
raise RuntimeError('Failed to get the exchange rate from CoinAPI')
self.conv_amount = float(rate * self.amount)
def rotate_token(lst, active) -> None:
active[0] = (active[0] + 1) % len(lst)

View file

@ -1,14 +0,0 @@
def format_number(number):
number_str = str(number)
if '.' in number_str:
integer_part, fractional_part = number_str.split('.')
else:
integer_part, fractional_part = number_str, ''
formatted_integer_part = '{:,}'.format(int(integer_part)).replace(',', ' ')
if fractional_part:
return formatted_integer_part + '.' + fractional_part
else:
return formatted_integer_part

191
main.py
View file

@ -1,79 +1,130 @@
from aiogram import Bot, Dispatcher, types #!/usr/bin/env python3
import asyncio import asyncio
import hashlib
import json
import string
import aiohttp
import requests
import re
import logging
import yaml import yaml
import hashlib from aiogram import Dispatcher, types, Bot
import aiohttp from aiogram.filters import CommandStart
import json # Constants
import re DDG_URL = 'https://duckduckgo.com/js/spice/currency'
COINAPI_URL = 'https://rest.coinapi.io/v1/exchangerate'
from word2number import w2n
from function.convert import Converter
config = yaml.safe_load(open("config.yaml")) config = yaml.safe_load(open("config.yaml"))
log = logging.getLogger('shirino')
handler = logging.StreamHandler()
fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
handler.setFormatter(fmt)
log.addHandler(handler)
if config['debug']:
handler.setLevel(logging.DEBUG)
log.setLevel(logging.DEBUG)
coinapi_len = len(config['coinapi_keys'])
coinapi_active = [0]
dp = Dispatcher() dp = Dispatcher()
@dp.message() class CurrencyConverter:
async def message_conv(message: types.Message): def __init__(self):
with open('currency.json', 'r', encoding='utf-8') as f: self.amount = 1.0
currency_json = json.load(f) self.conv_amount = 0.0
self.from_currency = 'RUB'
self.conv_currency = 'USD'
text = message.text.lower() def convert(self):
args = text.split() if not self.ddgapi():
self.coinapi()
number_match = re.match(r'\d+\.?\d*|\w+', args[0]) str_amount = f'{self.conv_amount}'
if number_match: point = str_amount.find(".")
number_text = number_match.group(0) after_point = str_amount[point + 1:]
try: fnz = min(
amount = float(number_text) (
except ValueError: after_point.index(i)
try: for i in string.digits[1:]
amount = float(w2n.word_to_num(number_text)) if i in after_point
except ValueError: ),
await message.reply("Не удалось распознать числовое значение.") default=-1,
return )
else:
await message.reply("Не удалось найти числовое значение.") if fnz == -1:
return return
if len(args) == 4: ndigits = fnz + config['ndigits']
source_currency_alias = args[1]
target_currency_alias = args[3]
elif len(args) == 3:
source_currency_alias = args[0]
target_currency_alias = args[2]
else:
await message.reply("Не удалось определить исходную и целевую валюту.")
return
source_currency_code = None self.conv_amount = round(self.conv_amount, ndigits)
target_currency_code = None
for currency_code, aliases in currency_json.items(): def ddgapi(self):
if source_currency_alias in aliases: res = requests.get(f'{DDG_URL}/{self.amount}/{self.from_currency}/{self.conv_currency}')
source_currency_code = currency_code data = json.loads(re.findall(r'\(\s*(.*)\s*\);$', res.text)[0])
if target_currency_alias in aliases:
target_currency_code = currency_code
if not source_currency_code or not target_currency_code or amount is None: del data['terms']
await message.reply("Не удалось найти сумму или валюты по указанным данным.") del data['privacy']
return del data['timestamp']
conv = Converter() log.debug(data)
conv.amount = amount
conv.from_currency = source_currency_code.upper()
conv.conv_currency = target_currency_code.upper()
conv.convert()
result = f'{conv.amount} {conv.from_currency} = {conv.conv_amount} {conv.conv_currency}' if len(data.get('to')) == 0:
await message.reply(result) return False
conv = data.get('to')[0]
conv_amount = conv.get("mid")
if conv_amount is None:
raise RuntimeError('Ошибка при конвертации валюты через DuckDuckGo')
log.debug(conv)
log.debug(conv_amount)
self.conv_amount = float(conv_amount)
return True
def coinapi(self, depth: int = config['coinapi_keys']):
if depth <= 0:
raise RecursionError('Рейтлимит на всех токенах')
resp = requests.get(
(
f'{COINAPI_URL}/{self.from_currency}'
f'/{self.conv_currency}'
),
headers={
'X-CoinAPI-Key': config['coinapi_keys'][coinapi_active[0]],
},
timeout=config['timeout'],
)
if resp.status_code == 429:
log.warning('CoinAPI ratelimited, rotating token')
rotate_token(config['coinapi_keys'], coinapi_active)
self.coinapi(depth - 1)
data = resp.json()
rate = data.get('rate')
if rate is None:
raise RuntimeError('Не удалось получить курс валюты от CoinAPI')
self.conv_amount = float(rate * self.amount)
async def inline_reply(result_id: str, title: str, description: str or None, inline_query: types.InlineQuery) -> None: def rotate_token(lst, active):
active[0] = (active[0] + 1) % len(lst)
async def inline_reply(result_id: str, title: str, description: str or None, inline_query: types.InlineQuery):
article = [None] article = [None]
article[0] = types.InlineQueryResultArticle( article[0] = types.InlineQueryResultArticle(
id=result_id, id=result_id,
@ -92,22 +143,22 @@ async def inline_reply(result_id: str, title: str, description: str or None, inl
@dp.inline_query() @dp.inline_query()
async def currency(inline_query: types.InlineQuery) -> None: async def currency(inline_query: types.InlineQuery):
global result
query = inline_query.query query = inline_query.query
args = query.split() args = query.split()
result_id = hashlib.md5(query.encode()).hexdigest() result_id = hashlib.md5(query.encode()).hexdigest()
conv = Converter() conv = CurrencyConverter()
try: try:
log.debug(len(args))
if len(args) <= 1: if len(args) <= 1:
await inline_reply(result_id, await inline_reply(result_id,
"2 or 3 arguments are required", "Требуется 2, либо 3 аргумента",
f"@shirino_bot USD RUB \n@shirino_bot 12 USD RUB", inline_query) f"@shirino_bot USD RUB \n@shirino_bot 12 USD RUB", inline_query)
if len(args) == 3: if len(args) == 3:
conv.amount = float(args[0].replace(',', '.')) conv.amount = float(args[0])
conv.from_currency = args[1].upper() conv.from_currency = args[1].upper()
conv.conv_currency = args[2].upper() conv.conv_currency = args[2].upper()
conv.convert() conv.convert()
@ -121,23 +172,35 @@ async def currency(inline_query: types.InlineQuery) -> None:
f'{conv.conv_amount} {conv.conv_currency}' f'{conv.conv_amount} {conv.conv_currency}'
) )
except aiohttp.client_exceptions.ClientError: except aiohttp.client_exceptions.ClientError as ex:
await inline_reply(result_id, await inline_reply(result_id,
"Rate-limit from the Telegram API, repeat the request later", "Rate-limit от API Telegram, повторите запрос позже",
None, None,
inline_query) inline_query)
log.debug(ex)
await asyncio.sleep(1) await asyncio.sleep(1)
except Exception: except Exception as ex:
await inline_reply(result_id, "Invalid data format", log.debug(ex)
await inline_reply(result_id, "Неверный формат данных",
"@shirino_bot USD RUB \n@shirino_bot 12 USD RUB", "@shirino_bot USD RUB \n@shirino_bot 12 USD RUB",
inline_query) inline_query)
await inline_reply(result_id, result, None, inline_query) await inline_reply(result_id, result, None, inline_query)
@dp.message(CommandStart())
async def start(message: types.Message):
await message.answer("Привет! Бот может показывать курс обмена криптовалюты и фиатной валюты. "
"Бот используется с помощью инлайн-команд: "
"`@shirino_bot 12 usd rub` или `@shirino_bot usd rub`"
"\n\nИсходный код опубликован на [Github](https://github.com/redume/shirino)",
parse_mode="markdown")
async def main() -> None: async def main() -> None:
log.debug(config)
bot = Bot(config['telegram_token']) bot = Bot(config['telegram_token'])
await dp.start_polling(bot) await dp.start_polling(bot)