From f3ca3b8f76256f9e90e7ca9396907a486f44f73a Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 13:53:30 +0300 Subject: [PATCH 01/27] chore: Add cert and ds_store to gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d306159..4bb8eee 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ __pycache__ .mypy_cache +.DS_Store venv -config.yaml \ No newline at end of file +config.yaml + +CertSSL \ No newline at end of file From 380b4cba1fddd317afe570fa1cc966abd38bbbaf Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 13:55:18 +0300 Subject: [PATCH 02/27] chore: Removed all functionality --- function/get_chart.py | 27 -------------- function/inline_query.py | 36 ------------------- utils/convert.py | 78 ---------------------------------------- utils/format_number.py | 14 -------- 4 files changed, 155 deletions(-) delete mode 100644 function/get_chart.py delete mode 100644 function/inline_query.py delete mode 100644 utils/convert.py delete mode 100644 utils/format_number.py diff --git a/function/get_chart.py b/function/get_chart.py deleted file mode 100644 index 5c24938..0000000 --- a/function/get_chart.py +++ /dev/null @@ -1,27 +0,0 @@ -import yaml -import requests -from http import HTTPStatus - -config = yaml.safe_load(open('config.yaml')) - -def get_chart(from_currency: str, conv_currency: str) -> (dict, None): - try: - response = requests.get(f'{config["kekkai_instance"]}/api/getChart/week/', params={ - 'from_currency': from_currency, - 'conv_currency': conv_currency - }, timeout=3) - - if not HTTPStatus(response.status_code).is_success: - return None - - try: - data = response.json() - return data.get('message', None) - except ValueError: - return None - except requests.exceptions.ConnectionError: - print("API connection error.") - return None - except requests.exceptions.RequestException as e: - print(f"There was an error: {e}") - return None diff --git a/function/inline_query.py b/function/inline_query.py deleted file mode 100644 index 5aed88f..0000000 --- a/function/inline_query.py +++ /dev/null @@ -1,36 +0,0 @@ -import re - -from aiogram import types - - -async def reply(result_id: str, args: list, query: types.InlineQuery) -> None: - if not args: - return - - articles = [] - - for idx, arg in enumerate(args): - title = arg[0] - description = arg[1] if arg[1] else None - img = arg[2] if arg[2] else None - - - article = types.InlineQueryResultArticle( - id=f"{result_id}_{idx}", - title=re.sub(r'\[([^\[]+)\]\([^\)]+\)', '', title).replace('График', ''), - thumbnail_url=img, - description=description, - input_message_content=types.InputTextMessageContent( - message_text=title, - parse_mode='markdown' - ) - ) - - articles.append(article) - - await query.answer( - results=articles, - parse_mode='markdown', - cache_time=0, - is_personal=True - ) \ No newline at end of file diff --git a/utils/convert.py b/utils/convert.py deleted file mode 100644 index c7cee9e..0000000 --- a/utils/convert.py +++ /dev/null @@ -1,78 +0,0 @@ -import yaml -import requests - -import json -import re - -from datetime import datetime -from http import HTTPStatus - -from decimal import Decimal, ROUND_DOWN -from utils.format_number import format_number - -config = yaml.safe_load(open('config.yaml')) - - -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.kekkai() and not self.ddg(): - raise RuntimeError('Failed to convert via Kekkai or DDG') - - number = Decimal(str(self.conv_amount)) - self.conv_amount = format_number(number.quantize(Decimal('1.00'), rounding=ROUND_DOWN)) - - - def ddg(self) -> bool: - try: - res = requests.get( - f'https://duckduckgo.com/js/spice/currency/' - f'{self.amount}/{self.from_currency}/{self.conv_currency}', - timeout=3 - ) - - data = json.loads(re.findall(r'\(\s*(.*)\s*\);$', res.text)[0]) - - for key in ['terms', 'privacy', 'timestamp']: - data.pop(key, None) - - if not data.get('to'): - return False - - self.conv_amount = float(data['to'][0].get('mid', 0.0)) - return True - - except (requests.exceptions.RequestException, json.JSONDecodeError, IndexError) as e: - print(f"Error when requesting DDG: {e}") - return False - - def kekkai(self) -> bool: - date = datetime.today().strftime('%Y-%m-%d') - try: - res = requests.get( - f"{config['kekkai_instance']}/api/getRate/", - params={ - 'from_currency': self.from_currency, - 'conv_currency': self.conv_currency, - 'date': date, - 'conv_amount': self.amount - }, - timeout=3 - ) - - if res.status_code != HTTPStatus.OK: - return False - - data = res.json() - self.conv_amount = data.get('conv_amount', 0.0) - return True - - except (requests.exceptions.ConnectionError, json.JSONDecodeError) as e: - print(f"Error when querying Kekkai: {e}") - return False diff --git a/utils/format_number.py b/utils/format_number.py deleted file mode 100644 index 8d475b9..0000000 --- a/utils/format_number.py +++ /dev/null @@ -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 From df8fae1d7fd33634e7aee2a405e56a1fbb6f95a2 Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 13:55:33 +0300 Subject: [PATCH 03/27] chore: Add nginx --- docker-compose.yaml | 14 +++++++++++++- nginx.conf | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 nginx.conf diff --git a/docker-compose.yaml b/docker-compose.yaml index a00bb48..053dcdf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,4 +2,16 @@ services: shirino: build: . image: ghcr.io/shirino/shirino:latest - restart: unless-stopped \ No newline at end of file + restart: unless-stopped + nginx: + image: nginx:latest + ports: + - '80:80' + - '443:443' + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./CertSSL:/etc/nginx/ssl + +volumes: + shirino: + driver: locale \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..9a83b78 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +events {} + +http { + server { + listen 80; + server_name shirino.redume.su; + + return 301 https://$host$request_uri$is_args$args; + } + + server { + listen 443 ssl; + http2 on; + server_name shirino.redume.susu; + + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + + location /webhook { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_buffering off; + } + } +} From ed493b862e0b5ad11c005fccb931eec829c04b38 Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 13:55:59 +0300 Subject: [PATCH 04/27] chore: Using webhooks --- main.py | 129 ++++++++++++++++++-------------------------------------- 1 file changed, 41 insertions(+), 88 deletions(-) diff --git a/main.py b/main.py index 4889b47..ac02228 100644 --- a/main.py +++ b/main.py @@ -1,104 +1,57 @@ -from aiogram import Bot, Dispatcher, types +import logging +import sys import yaml -import asyncio -import hashlib +from aiohttp import web -from function.get_chart import get_chart -from utils.convert import Converter -from utils.format_number import format_number -from function.inline_query import reply +from aiogram import Bot, Dispatcher, Router +from aiogram.client.default import DefaultBotProperties +from aiogram.enums import ParseMode +from aiogram.filters import CommandStart +from aiogram.types import Message +from aiogram.utils.markdown import hbold +from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application -dp = Dispatcher() config = yaml.safe_load(open('config.yaml')) -@dp.inline_query() -async def currency(query: types.InlineQuery) -> None: - global from_currency, conv_currency +router = Router() +@router.message() +async def echo_handler(message: Message) -> None: try: - text = query.query.lower() - args = text.split() - result_id = hashlib.md5(text.encode()).hexdigest() + # Send a copy of the received message + await message.send_copy(chat_id=message.chat.id) + except TypeError: + # But not all the types is supported to be copied so need to handle it + await message.answer("Nice try!") - conv = Converter() - - if len(args) < 2: - return await reply(result_id, - [("2 or 3 arguments are required.", - '@shirino_bot USD RUB \n' - '@shirino_bot 12 USD RUB', - None, None)], - query) - - if len(args) == 3: - conv.amount = float(args[0].replace(',', '.')) - 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) - - if not from_currency or not conv_currency: - return await reply(result_id, [('The currency exchange rate could not be found.', None, None)], query) - - conv.from_currency = from_currency.upper() - conv.conv_currency = conv_currency.upper() - conv.convert() - - chart = get_chart(from_currency, conv_currency) - - if not chart: - return await reply(result_id, - [ - ( - f'{format_number(conv.amount)} {conv.from_currency} ' - f'= {conv.conv_amount} {conv.conv_currency}' \ - f'\n{f'[График]({chart})' if chart else ''}', - None, - chart - ) - ], - query) - - await reply(result_id, - [ - ( - f'{format_number(conv.amount)} {conv.from_currency} ' - f'= {conv.conv_amount} {conv.conv_currency}' \ - f'\n{f'[График]({chart})' if chart else ''}', - None, - chart - ), - ( - f'{format_number(conv.amount)} {conv.from_currency} ' - f'= {conv.conv_amount} {conv.conv_currency}', - None, - None - ) - ], - query) - - except Exception as e: - print(e) +async def on_startup(bot: Bot) -> None: + # Убедитесь, что передаете HTTPS URL + await bot.set_webhook(f"{config['webhook']['base_url']}{config['webhook']['path']}", secret_token=config['webhook']['secret_token']) +def main() -> None: + dp = Dispatcher() -async def main() -> None: - bot = Bot(config['telegram_token']) - await dp.start_polling(bot) + dp.include_router(router) + dp.startup.register(on_startup) + + bot = Bot(token=config['telegram_token'], default=DefaultBotProperties(parse_mode=ParseMode.HTML)) + + app = web.Application() + webhook_requests_handler = SimpleRequestHandler( + dispatcher=dp, + bot=bot, + secret_token=config['webhook']['secret_token'] + ) + webhook_requests_handler.register(app, path=config['webhook']['path']) + + setup_application(app, dp, bot=bot) + + web.run_app(app, host='127.0.0.1', port=8080) if __name__ == '__main__': - asyncio.run(main()) + logging.basicConfig(level=logging.INFO, stream=sys.stdout) + main() From f751d0f8931f13439831d9247bbca36f80e9e672 Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 15:34:06 +0300 Subject: [PATCH 05/27] chore: change port and nginx url for proxy --- main.py | 2 +- nginx.conf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index ac02228..b9ef92b 100644 --- a/main.py +++ b/main.py @@ -49,7 +49,7 @@ def main() -> None: setup_application(app, dp, bot=bot) - web.run_app(app, host='127.0.0.1', port=8080) + web.run_app(app, host='0.0.0.0', port=443) if __name__ == '__main__': diff --git a/nginx.conf b/nginx.conf index 9a83b78..f70b053 100644 --- a/nginx.conf +++ b/nginx.conf @@ -3,7 +3,7 @@ events {} http { server { listen 80; - server_name shirino.redume.su; + server_name example.com; return 301 https://$host$request_uri$is_args$args; } @@ -11,13 +11,13 @@ http { server { listen 443 ssl; http2 on; - server_name shirino.redume.susu; + server_name example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location /webhook { - proxy_pass http://127.0.0.1:8080; + proxy_pass http://shirino:443; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From 288996f226e2ccca87b8054f02f663ae4b427821 Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 15:34:22 +0300 Subject: [PATCH 06/27] chore: fix driver locale and add volumes config --- docker-compose.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 053dcdf..c17e976 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,6 +3,9 @@ services: build: . image: ghcr.io/shirino/shirino:latest restart: unless-stopped + volumes: + - './config.yaml:/config.yaml' + nginx: image: nginx:latest ports: @@ -14,4 +17,4 @@ services: volumes: shirino: - driver: locale \ No newline at end of file + driver: local \ No newline at end of file From 3d6c6b11ecf4de53ed1ac7cd976fc598c779a3de Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 20:09:14 +0300 Subject: [PATCH 07/27] chore: use alpine version for python --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4dbeeca..c279e95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.12-alpine WORKDIR /shirino From 90c1717b6b87c4ddbc7dde9a0e63f789bedd7cc4 Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 22:52:12 +0300 Subject: [PATCH 08/27] refactor(convert): Rewrote the requests to aiohttp. Reverted the old query algorithm, first kekkai, then ddg --- functions/convert.py | 74 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 functions/convert.py diff --git a/functions/convert.py b/functions/convert.py new file mode 100644 index 0000000..7c18700 --- /dev/null +++ b/functions/convert.py @@ -0,0 +1,74 @@ +from utils.format_number import format_number + +import yaml + +import aiohttp +import json +import re + +from datetime import datetime +from http import HTTPStatus +from decimal import Decimal, ROUND_DOWN +from utils.format_number import format_number + +config = yaml.safe_load(open('config.yaml')) + +class Converter: + def __init__(self): + self.amount: float = 1.0 + self.conv_amount: float = 0.0 + self.from_currency: str = '' + self.conv_currency: str = '' + + async def convert(self) -> None: + if not await self.kekkai(): + await self.ddg() + + number = Decimal(str(self.conv_amount)) + self.conv_amount = format_number(number.quantize(Decimal('1.0000'), rounding=ROUND_DOWN)) + + + async def kekkai(self) -> bool: + date = datetime.today().strftime('%Y-%m-%d') + + 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 + }, + timeout=3) as res: + if not HTTPStatus(res.status).is_success: + return False + + data = await res.json() + self.conv_amount = data.get('conv_amount', 0.0) + + return True + + + async def ddg(self) -> None: + async with aiohttp.ClientSession(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}' + ) as res: + + data_text = await res.text() + + data = json.loads(re.findall(r'\(\s*(.*)\s*\);$', data_text)[0]) + + for key in ['terms', 'privacy', 'timestamp']: + data.pop(key, None) + + if len(data.get('to')) == 0: + raise RuntimeError('Failed to get the exchange rate from DuckDuckGo') + + 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) From 8cc073c47335ea5773255f9e71fe10800cb46d8b Mon Sep 17 00:00:00 2001 From: Redume Date: Wed, 8 Jan 2025 22:58:12 +0300 Subject: [PATCH 09/27] chore: Returned the formatting and the inline query response --- utils/format_number.py | 14 ++++++++++++++ utils/inline_query.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 utils/format_number.py create mode 100644 utils/inline_query.py diff --git a/utils/format_number.py b/utils/format_number.py new file mode 100644 index 0000000..0694895 --- /dev/null +++ b/utils/format_number.py @@ -0,0 +1,14 @@ +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 \ No newline at end of file diff --git a/utils/inline_query.py b/utils/inline_query.py new file mode 100644 index 0000000..edf921d --- /dev/null +++ b/utils/inline_query.py @@ -0,0 +1,34 @@ +from aiogram import types + + +async def reply(result_id: str, args: list, query: types.InlineQuery) -> None: + if not args: + return + + articles = [] + + for idx, arg in enumerate(args): + title = arg[0] + description = arg[1] if arg[1] else None + img = arg[2] if arg[2] else None + + + article = types.InlineQueryResultArticle( + id=f"{result_id}_{idx}", + title=title, + thumbnail_url=img, + description=description, + input_message_content=types.InputTextMessageContent( + message_text=title, + parse_mode='markdown' + ) + ) + + articles.append(article) + + await query.answer( + results=articles, + parse_mode='markdown', + cache_time=0, + is_personal=True + ) \ No newline at end of file From c7ab4b72f9f0644c0661bfa13c5ee4224c9305d8 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:02:42 +0300 Subject: [PATCH 10/27] chore: deleted timeout in request --- functions/convert.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/functions/convert.py b/functions/convert.py index 7c18700..1e898c4 100644 --- a/functions/convert.py +++ b/functions/convert.py @@ -37,8 +37,7 @@ class Converter: 'conv_currency': self.conv_currency, 'date': date, 'conv_amount': self.amount - }, - timeout=3) as res: + }) as res: if not HTTPStatus(res.status).is_success: return False From f122614b9fb84f23c9fc00db9595872a3e1d622d Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:03:19 +0300 Subject: [PATCH 11/27] chore: Made a regular expression to remove the graph reference from the previews --- utils/inline_query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/inline_query.py b/utils/inline_query.py index edf921d..282ad95 100644 --- a/utils/inline_query.py +++ b/utils/inline_query.py @@ -1,5 +1,6 @@ from aiogram import types +import re async def reply(result_id: str, args: list, query: types.InlineQuery) -> None: if not args: @@ -15,7 +16,7 @@ async def reply(result_id: str, args: list, query: types.InlineQuery) -> None: article = types.InlineQueryResultArticle( id=f"{result_id}_{idx}", - title=title, + title=re.sub(r'\bГрафик\b|\[([^\]]+)\]\([^)]+\)', '', title, flags=re.IGNORECASE), thumbnail_url=img, description=description, input_message_content=types.InputTextMessageContent( From d8d6635987fc6d68ec5a8346e2b27998cf2f0497 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:04:31 +0300 Subject: [PATCH 12/27] refactor: Rewrote the creation of graphs on aiohttp --- functions/create_chart.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 functions/create_chart.py diff --git a/functions/create_chart.py b/functions/create_chart.py new file mode 100644 index 0000000..078effb --- /dev/null +++ b/functions/create_chart.py @@ -0,0 +1,19 @@ +import yaml +import aiohttp + +from http import HTTPStatus + +config = yaml.safe_load(open('config.yaml')) + +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 session.get(f'{config["kekkai_instance"]}/api/getChart/week/', params={ + 'from_currency': from_currency, + 'conv_currency': conv_currency + }) as res: + if not HTTPStatus(res.status).is_success: + return None + + data = await res.json() + + return data.get('message', None) From 234e8bf244ef395663dd395f732329b463fbe05b Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:18:24 +0300 Subject: [PATCH 13/27] fix: handle exponential notation and large numbers in format_number function --- utils/format_number.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/utils/format_number.py b/utils/format_number.py index 0694895..fe0de81 100644 --- a/utils/format_number.py +++ b/utils/format_number.py @@ -1,14 +1,12 @@ +from decimal import Decimal + 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: + number = Decimal(str(number)) + + formatted_integer_part = '{:,.0f}'.format(number).replace(',', ' ') + + if '.' in str(number): + fractional_part = str(number).split('.')[1] return formatted_integer_part + '.' + fractional_part else: - return formatted_integer_part \ No newline at end of file + return formatted_integer_part From a1ad2d16944670fd4a3efa479e7420f5d1f89e7a Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:19:00 +0300 Subject: [PATCH 14/27] refactor: Bot rewritten to webhooks, duplicate code removed --- main.py | 84 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/main.py b/main.py index b9ef92b..80a3632 100644 --- a/main.py +++ b/main.py @@ -1,34 +1,83 @@ -import logging -import sys +from functions.convert import Converter +from utils.format_number import format_number +from utils.inline_query import reply +from functions.create_chart import create_chart import yaml from aiohttp import web -from aiogram import Bot, Dispatcher, Router +from aiogram import Bot, Dispatcher, Router, types from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode -from aiogram.filters import CommandStart -from aiogram.types import Message -from aiogram.utils.markdown import hbold from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application +import hashlib + config = yaml.safe_load(open('config.yaml')) +bot = Bot(token=config['telegram_token'], default=DefaultBotProperties(parse_mode=ParseMode.HTML)) router = Router() -@router.message() -async def echo_handler(message: Message) -> None: - try: - # Send a copy of the received message - await message.send_copy(chat_id=message.chat.id) - except TypeError: - # But not all the types is supported to be copied so need to handle it - await message.answer("Nice try!") +@router.inline_query() +async def currency(query: types.InlineQuery) -> None: + text = query.query.lower() + args = text.split() + result_id = hashlib.md5(text.encode()).hexdigest() + + if len(args) < 2: + get_bot = await bot.get_me() + 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: + conv.amount = float(args[0].replace(',', '.')) + 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) + + if not conv_currency or not from_currency: + return await reply(result_id, [('The currency exchange rate could not be found.', None, None)], query) + + conv.from_currency = from_currency.upper() + conv.conv_currency = conv_currency.upper() + await conv.convert() + + chart = await create_chart(from_currency, conv_currency) + + message = f'{format_number(conv.amount)} {conv.from_currency} = {conv.conv_amount} {conv.conv_currency}' + + results = [(message, None, None)] + + if chart: + results.insert(0, (f'{message}\n[График]({chart})', None, chart)) + + await reply(result_id, results, query) + async def on_startup(bot: Bot) -> None: - # Убедитесь, что передаете HTTPS URL - await bot.set_webhook(f"{config['webhook']['base_url']}{config['webhook']['path']}", secret_token=config['webhook']['secret_token']) + await bot.set_webhook( + f"{config['webhook']['base_url']}{config['webhook']['path']}", + secret_token=config['webhook']['secret_token'], + allowed_updates=['inline_query'] + ) def main() -> None: @@ -37,8 +86,6 @@ def main() -> None: dp.include_router(router) dp.startup.register(on_startup) - bot = Bot(token=config['telegram_token'], default=DefaultBotProperties(parse_mode=ParseMode.HTML)) - app = web.Application() webhook_requests_handler = SimpleRequestHandler( dispatcher=dp, @@ -53,5 +100,4 @@ def main() -> None: if __name__ == '__main__': - logging.basicConfig(level=logging.INFO, stream=sys.stdout) main() From 0ca150eec8822ca114bec20ff2a38e0cddc6a956 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:29:34 +0300 Subject: [PATCH 15/27] fix: did a check for negative numbers --- main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index 80a3632..a90008a 100644 --- a/main.py +++ b/main.py @@ -39,7 +39,13 @@ async def currency(query: types.InlineQuery) -> None: from_currency, conv_currency = '', '' if len(args) == 3: - conv.amount = float(args[0].replace(',', '.')) + try: + conv.amount = float(args[0].replace(',', '.')) + if conv.amount < 0: + raise ValueError("Negative amounts are not supported.") + except ValueError: + return await reply(result_id, [("Please enter a positive amount.", None, None)], query) + from_currency = args[1] conv_currency = args[2] elif len(args) == 2: From 0537bfe3cf74aae08d3fc6acdbe7efa86a788c97 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 00:55:50 +0300 Subject: [PATCH 16/27] fix: Fixed conditions with negative numbers and if text is written instead of number --- main.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index a90008a..ff5a6d1 100644 --- a/main.py +++ b/main.py @@ -25,14 +25,15 @@ async def currency(query: types.InlineQuery) -> None: args = text.split() result_id = hashlib.md5(text.encode()).hexdigest() + get_bot = await bot.get_me() + if len(args) < 2: - get_bot = await bot.get_me() - 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) + 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() @@ -42,9 +43,13 @@ async def currency(query: types.InlineQuery) -> None: try: conv.amount = float(args[0].replace(',', '.')) if conv.amount < 0: - raise ValueError("Negative amounts are not supported.") + return await reply(result_id, [("Negative amounts are not supported.", None, None)], query) + except ValueError: - return await reply(result_id, [("Please enter a positive amount.", None, None)], query) + 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] From 169166af3b8f63ab8c73310e3860a0acbbf6ce6f Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 13:16:13 +0300 Subject: [PATCH 17/27] chore: delete requests in requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a713e12..8121224 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ aiohttp~=3.9.5 -requests~=2.32.3 PyYAML~=6.0.1 aiogram~=3.7.0 \ No newline at end of file From 6a2291c6bfc96d9b95876897e53680bad77611fd Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 13:26:33 +0300 Subject: [PATCH 18/27] chore: new example config --- config_sample.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/config_sample.yaml b/config_sample.yaml index 9e09ebb..637f17c 100644 --- a/config_sample.yaml +++ b/config_sample.yaml @@ -1,4 +1,6 @@ -debug: false # debug logging -timeout: 2 # http requests timeout -telegram_token: # telegram bot token -kekkai_instance: 'https://kekkai-api.redume.su/' \ No newline at end of file +webhook: + secret_token: 'j_!?TD]nK6!nu'EZ654b' + base_url: 'example.com' + path: '/webhook' +telegram_token: +kekkai_instance: 'https://kekkai-api.redume.su' \ No newline at end of file From 7ed98e5b88cdd4547de3a4b7e506ba0f73bee2c4 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 13:26:57 +0300 Subject: [PATCH 19/27] chore: the schedule period has been changed from a week to a month --- functions/create_chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/create_chart.py b/functions/create_chart.py index 078effb..117dd4d 100644 --- a/functions/create_chart.py +++ b/functions/create_chart.py @@ -7,7 +7,7 @@ config = yaml.safe_load(open('config.yaml')) 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 session.get(f'{config["kekkai_instance"]}/api/getChart/week/', params={ + async with session.get(f'{config["kekkai_instance"]}/api/getChart/month/', params={ 'from_currency': from_currency, 'conv_currency': conv_currency }) as res: From 39ea50f0d77d93a723c42b08379ddd075861f835 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 13:37:08 +0300 Subject: [PATCH 20/27] chore: updated the aiogram version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8121224..21259b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ aiohttp~=3.9.5 PyYAML~=6.0.1 -aiogram~=3.7.0 \ No newline at end of file +aiogram~=3.15.0 \ No newline at end of file From ad8dc2da35900d88459cd2e2e1b7d47d3a6afe9f Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 15:44:04 +0300 Subject: [PATCH 21/27] chore: deleted special symbol --- config_sample.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config_sample.yaml b/config_sample.yaml index 637f17c..fd02a56 100644 --- a/config_sample.yaml +++ b/config_sample.yaml @@ -1,5 +1,5 @@ webhook: - secret_token: 'j_!?TD]nK6!nu'EZ654b' + secret_token: 'jTDnK6nuEZ654b' base_url: 'example.com' path: '/webhook' telegram_token: From caac4f9a1fca206aadca2ff9ca705947e4a0f02d Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 20:41:29 +0300 Subject: [PATCH 22/27] feat: Made a command to start --- main.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/main.py b/main.py index ff5a6d1..c383a44 100644 --- a/main.py +++ b/main.py @@ -11,6 +11,7 @@ from aiogram import Bot, Dispatcher, Router, types from aiogram.client.default import DefaultBotProperties from aiogram.enums import ParseMode from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application +from aiogram.filters import CommandStart import hashlib @@ -19,6 +20,19 @@ bot = Bot(token=config['telegram_token'], default=DefaultBotProperties(parse_mod router = Router() +@router.message(CommandStart()) +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() @@ -28,8 +42,8 @@ async def currency(query: types.InlineQuery) -> None: get_bot = await bot.get_me() if len(args) < 2: - return await reply(result_id, - [("2 or 3 arguments are required.", + 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)], @@ -50,7 +64,7 @@ async def currency(query: types.InlineQuery) -> None: 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: @@ -74,12 +88,12 @@ async def currency(query: types.InlineQuery) -> None: chart = await create_chart(from_currency, conv_currency) message = f'{format_number(conv.amount)} {conv.from_currency} = {conv.conv_amount} {conv.conv_currency}' - + results = [(message, None, None)] - + if chart: results.insert(0, (f'{message}\n[График]({chart})', None, chart)) - + await reply(result_id, results, query) @@ -87,7 +101,7 @@ async def on_startup(bot: Bot) -> None: await bot.set_webhook( f"{config['webhook']['base_url']}{config['webhook']['path']}", secret_token=config['webhook']['secret_token'], - allowed_updates=['inline_query'] + allowed_updates=['inline_query', 'message'] ) @@ -96,7 +110,7 @@ def main() -> None: dp.include_router(router) dp.startup.register(on_startup) - + app = web.Application() webhook_requests_handler = SimpleRequestHandler( dispatcher=dp, From 0eafaba090c9b2b2cf955b236bcbd40d372db255 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 20:47:35 +0300 Subject: [PATCH 23/27] feat: commands are now in mono-wide format --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index c383a44..5b54d05 100644 --- a/main.py +++ b/main.py @@ -26,8 +26,8 @@ async def start(message: types.Message): 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' + 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' ) From c9eb773f1040d45a40925f831240cd8fb5829bf0 Mon Sep 17 00:00:00 2001 From: Redume Date: Thu, 9 Jan 2025 23:03:38 +0300 Subject: [PATCH 24/27] fix: Error with Decimal if the number was too large --- functions/convert.py | 15 ++++++--------- utils/format_number.py | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/functions/convert.py b/functions/convert.py index 1e898c4..c6ed7ea 100644 --- a/functions/convert.py +++ b/functions/convert.py @@ -1,14 +1,12 @@ -from utils.format_number import format_number - -import yaml - -import aiohttp import json import re - from datetime import datetime +from decimal import Decimal from http import HTTPStatus -from decimal import Decimal, ROUND_DOWN + +import aiohttp +import yaml + from utils.format_number import format_number config = yaml.safe_load(open('config.yaml')) @@ -24,8 +22,7 @@ class Converter: if not await self.kekkai(): await self.ddg() - number = Decimal(str(self.conv_amount)) - self.conv_amount = format_number(number.quantize(Decimal('1.0000'), rounding=ROUND_DOWN)) + self.conv_amount = format_number(Decimal(self.conv_amount)) async def kekkai(self) -> bool: diff --git a/utils/format_number.py b/utils/format_number.py index fe0de81..6012c7f 100644 --- a/utils/format_number.py +++ b/utils/format_number.py @@ -2,11 +2,11 @@ from decimal import Decimal def format_number(number): number = Decimal(str(number)) - + formatted_integer_part = '{:,.0f}'.format(number).replace(',', ' ') - + if '.' in str(number): - fractional_part = str(number).split('.')[1] + fractional_part = str(number).split('.')[1][:3] return formatted_integer_part + '.' + fractional_part else: return formatted_integer_part From db79e742688ca91d38cd61977ddad6fff1324b21 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 10 Jan 2025 00:12:00 +0300 Subject: [PATCH 25/27] chore: Made a check if the currency rate was not found --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index 5b54d05..bc807dd 100644 --- a/main.py +++ b/main.py @@ -78,12 +78,12 @@ async def currency(query: types.InlineQuery) -> None: )], query) - if not conv_currency or not from_currency: - return await reply(result_id, [('The currency exchange rate could not be found.', None, None)], query) - conv.from_currency = from_currency.upper() conv.conv_currency = conv_currency.upper() - await conv.convert() + try: + await conv.convert() + except RuntimeError as e: + return await reply(result_id, [('The currency exchange rate could not be determined', None, None)], query) chart = await create_chart(from_currency, conv_currency) From ef980f1ef6cb76b9c6cf562326ab15d5402ff5ea Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 10 Jan 2025 00:18:15 +0300 Subject: [PATCH 26/27] fix: fixed the condition with getting currency from_currency --- functions/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/convert.py b/functions/convert.py index c6ed7ea..8d1931a 100644 --- a/functions/convert.py +++ b/functions/convert.py @@ -58,7 +58,7 @@ class Converter: for key in ['terms', 'privacy', 'timestamp']: data.pop(key, None) - if len(data.get('to')) == 0: + if not data.get('to'): raise RuntimeError('Failed to get the exchange rate from DuckDuckGo') conv = data.get('to')[0] From b5f75ef0bc7322cb1df29b38aef75632915c94d9 Mon Sep 17 00:00:00 2001 From: Redume Date: Fri, 10 Jan 2025 00:54:59 +0300 Subject: [PATCH 27/27] feat: add support for grouping separators and precise handling of small numbers in format_number --- utils/format_number.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/utils/format_number.py b/utils/format_number.py index 6012c7f..7759bab 100644 --- a/utils/format_number.py +++ b/utils/format_number.py @@ -3,10 +3,12 @@ from decimal import Decimal def format_number(number): number = Decimal(str(number)) - formatted_integer_part = '{:,.0f}'.format(number).replace(',', ' ') + formatted_integer_part = '{:,.0f}'.format(number // 1).replace(',', ' ') + fractional_part = f"{number % 1:.30f}".split('.')[1] - if '.' in str(number): - fractional_part = str(number).split('.')[1][:3] - return formatted_integer_part + '.' + fractional_part - else: - return formatted_integer_part + if int(fractional_part) == 0: + return formatted_integer_part + '' + + significant_start = next((i for i, char in enumerate(fractional_part) if char != '0'), len(fractional_part)) + result_fractional = fractional_part[:significant_start + 3] + return formatted_integer_part + '.' + result_fractional