diff --git a/src/bot.py b/src/bot.py index c5bcea6..8c4fa3c 100644 --- a/src/bot.py +++ b/src/bot.py @@ -81,14 +81,17 @@ async def request(self, method, data=None, *args, **kwargs): return result -tg = TellerBot(None, loop=asyncio.get_event_loop(), validate_token=False) +tg = TellerBot("0:", loop=asyncio.get_event_loop(), validate_token=False) dp = Dispatcher(tg) def setup(): """Set API token from config to bot and setup dispatcher.""" with open(config.TOKEN_FILENAME, "r") as token_file: - tg._ctx_token.set(token_file.read().strip()) + token = token_file.read().strip() + api.check_token(token) + tg._ctx_token.set(token) + tg.id = int(token.split(":")[0]) dp.storage = MongoStorage() diff --git a/src/escrow/blockchain/__init__.py b/src/escrow/blockchain/__init__.py index b440695..a27a700 100644 --- a/src/escrow/blockchain/__init__.py +++ b/src/escrow/blockchain/__init__.py @@ -240,9 +240,11 @@ async def _confirmation_callback( escrow_user = offer["counter"] other_user = offer["init"] - answer = i18n( - "transaction_passed {currency}", locale=escrow_user["locale"] - ).format(currency=offer[new_currency]) + answer = markdown.escape_md( + i18n("transaction_passed {currency}", locale=escrow_user["locale"]).format( + currency=offer[new_currency] + ) + ) await tg.send_message(escrow_user["id"], answer) is_confirmed = await create_task(self.is_block_confirmed(block_num, op)) if is_confirmed: @@ -260,19 +262,21 @@ async def _confirmation_callback( i18n("transaction_confirmed", locale=other_user["locale"]), self.trx_url(trx_id), ) - answer += "\n" + i18n( - "send {amount} {currency} {address}", locale=other_user["locale"] - ).format( - amount=offer[f"sum_{new_currency}"], - currency=offer[new_currency], - address=markdown.escape_md(escrow_user["receive_address"]), + answer += "\n" + markdown.escape_md( + i18n( + "send {amount} {currency} {address}", locale=other_user["locale"] + ).format( + amount=offer[f"sum_{new_currency}"], + currency=offer[new_currency], + address=escrow_user["receive_address"], + ) ) - answer += "." + answer += r"\." await tg.send_message( other_user["id"], answer, reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN, + parse_mode=ParseMode.MARKDOWN_V2, ) return True @@ -327,7 +331,7 @@ async def _refund_callback( answer += f"\n• {message_point}" answer += "\n\n" + i18n("refund_promise", locale=user["locale"]) - await tg.send_message(user["id"], answer, parse_mode=ParseMode.MARKDOWN) + await tg.send_message(user["id"], answer) is_confirmed = await create_task(self.is_block_confirmed(block_num, op)) await database.escrow.update_one( {"_id": offer["_id"]}, {"$set": {"transaction_time": time()}} @@ -343,9 +347,11 @@ async def _refund_callback( i18n("transaction_refunded", locale=user["locale"]), trx_url ) else: - answer = i18n("transaction_not_confirmed", locale=user["locale"]) - answer += " " + i18n("try_again", locale=user["locale"]) - await tg.send_message(user["id"], answer, parse_mode=ParseMode.MARKDOWN) + answer = i18n( + "transaction_not_confirmed", locale=user["locale"], escape_md=True + ) + answer += " " + i18n("try_again", locale=user["locale"], escape_md=True) + await tg.send_message(user["id"], answer, parse_mode=ParseMode.MARKDOWN_V2) class StreamBlockchain(BaseBlockchain): diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py index 3841a62..6b988bd 100644 --- a/src/handlers/__init__.py +++ b/src/handlers/__init__.py @@ -86,7 +86,7 @@ async def errors_handler(update: types.Update, exception: Exception): else: await tg.send_message( exceptions_chat_id, - "Error handling {} {} from {} ({}) in chat {}\n{}".format( + r"Error handling {} {} from {} \({}\) in chat {}\n{}".format( update_type, update.update_id, markdown.link(from_user.mention, from_user.url), @@ -94,7 +94,7 @@ async def errors_handler(update: types.Update, exception: Exception): chat_id, markdown.code(traceback.format_exc(limit=-3)), ), - parse_mode=types.ParseMode.MARKDOWN, + parse_mode=types.ParseMode.MARKDOWN_V2, ) await tg.send_message( chat_id, i18n("unexpected_error"), reply_markup=start_keyboard(), diff --git a/src/handlers/base.py b/src/handlers/base.py index e99d48e..48e876a 100644 --- a/src/handlers/base.py +++ b/src/handlers/base.py @@ -175,10 +175,12 @@ async def orders_list( order["buy"], ) + line = markdown.escape_md(line) + if user_id is not None and order["user_id"] == user_id: line = f"*{line}*" - lines.append(f"{i + 1}. {line}") + lines.append(fr"{i + 1}\. {line}") buttons.append( types.InlineKeyboardButton( "{}".format(i + 1), callback_data="get_order {}".format(order["_id"]) @@ -194,22 +196,21 @@ async def orders_list( keyboard.add(*buttons) keyboard.row(*inline_orders_buttons) - text = ( - "\\[" + text = markdown.escape_md( + "[" + i18n("page {number} {total}").format( number=math.ceil(start / config.ORDERS_COUNT) + 1, total=math.ceil(quantity / config.ORDERS_COUNT), ) + "]\n" - + "\n".join(lines) - ) + ) + "\n".join(lines) if message_id is None: await tg.send_message( chat_id, text, reply_markup=keyboard, - parse_mode=types.ParseMode.MARKDOWN, + parse_mode=types.ParseMode.MARKDOWN_V2, disable_web_page_preview=True, ) else: @@ -218,7 +219,7 @@ async def orders_list( chat_id, message_id, reply_markup=keyboard, - parse_mode=types.ParseMode.MARKDOWN, + parse_mode=types.ParseMode.MARKDOWN_V2, disable_web_page_preview=True, ) @@ -295,7 +296,7 @@ async def show_order( header += markdown.bold(i18n("archived", locale=locale)) + "\n" creator = await database.users.find_one({"id": order["user_id"]}) - header += "{} ({}) ".format( + header += r"{} \({}\) ".format( markdown.link(creator["mention"], types.User(id=creator["id"]).url), markdown.code(creator["id"]), ) @@ -303,9 +304,13 @@ async def show_order( act = i18n("sells {sell_currency} {buy_currency}", locale=locale) else: act = i18n("buys {buy_currency} {sell_currency}", locale=locale) - header += act.format(buy_currency=order["buy"], sell_currency=order["sell"]) + "\n" + header += ( + markdown.escape_md( + act.format(buy_currency=order["buy"], sell_currency=order["sell"]) + ) + + "\n" + ) - lines = [header] field_names = { "sum_buy": i18n("buy_amount", locale=locale), "sum_sell": i18n("sell_amount", locale=locale), @@ -355,6 +360,8 @@ async def show_order( ) ) + lines = [] + if edit and creator["id"] == user_id: buttons = [] for i, (field, value) in enumerate(lines_format.items()): @@ -449,7 +456,7 @@ async def show_order( ) ) - answer = "\n".join(lines) + answer = "\n".join((header, *map(markdown.escape_md, lines))) if message_id is not None: await tg.edit_message_text( @@ -457,7 +464,7 @@ async def show_order( chat_id, message_id, reply_markup=keyboard, - parse_mode=types.ParseMode.MARKDOWN, + parse_mode=types.ParseMode.MARKDOWN_V2, disable_web_page_preview=True, ) if new_edit_msg is not None: @@ -478,6 +485,6 @@ async def show_order( chat_id, answer, reply_markup=keyboard, - parse_mode=types.ParseMode.MARKDOWN, + parse_mode=types.ParseMode.MARKDOWN_V2, disable_web_page_preview=True, ) diff --git a/src/handlers/escrow.py b/src/handlers/escrow.py index c0ccd37..68d8544 100644 --- a/src/handlers/escrow.py +++ b/src/handlers/escrow.py @@ -292,21 +292,24 @@ async def ask_credentials( i18n( "request_full_card_number {currency} {user}", locale=request_user["locale"], - ).format(currency=currency, user=mention), + escape_md=True, + ).format(currency=markdown.escape_md(currency), user=mention), reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN, + parse_mode=ParseMode.MARKDOWN_V2, ) state = FSMContext(dp.storage, request_user["id"], request_user["id"]) await state.set_state(states.Escrow.full_card.state) answer = i18n( - "asked_full_card_number {user}", locale=answer_user["locale"] + "asked_full_card_number {user}", + locale=answer_user["locale"], + escape_md=True, ).format( user=markdown.link( request_user["mention"], User(id=request_user["id"]).url ) ) await tg.send_message( - answer_user["id"], answer, parse_mode=ParseMode.MARKDOWN, + answer_user["id"], answer, parse_mode=ParseMode.MARKDOWN_V2, ) return @@ -385,8 +388,10 @@ async def full_card_number_message(message: types.Message, offer: EscrowOffer): mention = markdown.link(user["mention"], User(id=user["id"]).url) await tg.send_message( message.chat.id, - i18n("wrong_full_card_number_receiver {user}").format(user=mention), - parse_mode=ParseMode.MARKDOWN, + i18n("wrong_full_card_number_receiver {user}", escape_md=True).format( + user=mention + ), + parse_mode=ParseMode.MARKDOWN_V2, ) @@ -404,10 +409,10 @@ async def full_card_number_sent(call: types.CallbackQuery, offer: EscrowOffer): ) await tg.send_message( call.message.chat.id, - i18n("exchange_continued {user}").format( + i18n("exchange_continued {user}", escape_md=True).format( user=markdown.link(counter["mention"], User(id=counter["id"]).url) ), - parse_mode=ParseMode.MARKDOWN, + parse_mode=ParseMode.MARKDOWN_V2, ) await offer.update_document({"$set": {"pending_input_from": counter["id"]}}) counter_state = FSMContext(dp.storage, counter["id"], counter["id"]) @@ -579,23 +584,28 @@ async def set_init_send_address( "escrow_offer_notification {user} {sell_amount} {sell_currency} " "for {buy_amount} {buy_currency}", locale=locale, + escape_md=True, ).format( user=mention, - sell_amount=offer.sum_sell, - sell_currency=offer.sell, - buy_amount=offer.sum_buy, - buy_currency=offer.buy, + sell_amount=markdown.escape_md(offer.sum_sell), + sell_currency=markdown.escape_md(offer.sell), + buy_amount=markdown.escape_md(offer.sum_buy), + buy_currency=markdown.escape_md(offer.buy), ) if offer.bank: - answer += " " + i18n("using {bank}", locale=locale).format(bank=offer.bank) - answer += "." + answer += " " + markdown.escape_md( + i18n("using {bank}", locale=locale).format(bank=offer.bank) + ) + answer += r"\." update_dict = {"init.send_address": address} if offer.type == "sell": insured = await get_insurance(offer) update_dict["insured"] = Decimal128(insured) if offer[f"sum_{offer.type}"].to_decimal() > insured: - answer += "\n" + i18n("exceeded_insurance {amount} {currency}").format( - amount=insured, currency=offer.escrow + answer += "\n" + markdown.escape_md( + i18n("exceeded_insurance {amount} {currency}", escape_md=True).format( + amount=insured, currency=offer.escrow + ) ) await offer.update_document( {"$set": update_dict, "$unset": {"pending_input_from": True}} @@ -604,7 +614,7 @@ async def set_init_send_address( offer.counter["id"], answer, reply_markup=buy_keyboard, - parse_mode=ParseMode.MARKDOWN, + parse_mode=ParseMode.MARKDOWN_V2, ) sell_keyboard = InlineKeyboardMarkup() sell_keyboard.add( @@ -746,12 +756,21 @@ async def set_counter_send_address( ) escrow_address = markdown.bold(escrow_instance.address) answer = i18n( - "send {amount} {currency} {address}", locale=escrow_user["locale"] - ).format(amount=offer.sum_fee_up, currency=offer.escrow, address=escrow_address) - answer += " " + i18n("with_memo", locale=escrow_user["locale"]) + "send {amount} {currency} {address}", + locale=escrow_user["locale"], + escape_md=True, + ).format( + amount=markdown.escape_md(offer.sum_fee_up), + currency=markdown.escape_md(offer.escrow), + address=escrow_address, + ) + answer += " " + i18n("with_memo", locale=escrow_user["locale"], escape_md=True) answer += ":\n" + markdown.code(memo) await tg.send_message( - escrow_user["id"], answer, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN + escrow_user["id"], + answer, + reply_markup=keyboard, + parse_mode=ParseMode.MARKDOWN_V2, ) if send_reply: keyboard = InlineKeyboardMarkup() @@ -766,7 +785,6 @@ async def set_counter_send_address( + " " + i18n("transaction_completion_notification_promise"), reply_markup=keyboard, - parse_mode=ParseMode.MARKDOWN, ) update = { "counter.send_address": address, @@ -888,34 +906,34 @@ async def final_offer_confirmation(call: types.CallbackQuery, offer: EscrowOffer async def complete_offer(call: types.CallbackQuery, offer: EscrowOffer): """Release escrow asset and finish exchange.""" if offer.type == "buy": - recipient_user = offer.counter + rcvr_user = offer.counter other_user = offer.init elif offer.type == "sell": - recipient_user = offer.init + rcvr_user = offer.init other_user = offer.counter await call.answer(i18n("escrow_completing")) escrow_instance = get_escrow_instance(offer.escrow) trx_url = await escrow_instance.transfer( - recipient_user["receive_address"], + rcvr_user["receive_address"], offer.sum_fee_down.to_decimal(), # type: ignore offer.escrow, memo=create_memo(offer, transfer=True), ) - answer = i18n("escrow_completed", locale=other_user["locale"]) - recipient_answer = i18n("escrow_completed", locale=recipient_user["locale"]) - recipient_answer += " " + markdown.link( - i18n("escrow_sent {amount} {currency}", locale=recipient_user["locale"]).format( + answer = i18n("escrow_completed", locale=other_user["locale"], escape_md=True) + rcvr_answer = i18n("escrow_completed", locale=rcvr_user["locale"], escape_md=True) + rcvr_answer += " " + markdown.link( + i18n("escrow_sent {amount} {currency}", locale=rcvr_user["locale"]).format( amount=offer.sum_fee_down, currency=offer.escrow ), trx_url, ) await offer.delete_document() await tg.send_message( - recipient_user["id"], - recipient_answer, + rcvr_user["id"], + rcvr_answer, reply_markup=start_keyboard(), - parse_mode=ParseMode.MARKDOWN, + parse_mode=ParseMode.MARKDOWN_V2, ) await tg.send_message(other_user["id"], answer, reply_markup=start_keyboard()) @@ -925,26 +943,28 @@ async def validate_offer(call: types.CallbackQuery, offer: EscrowOffer): """Ask support for manual verification of exchange.""" if offer.type == "buy": sender = offer.counter - receiver = offer.init + rcvr = offer.init currency = offer.sell elif offer.type == "sell": sender = offer.init - receiver = offer.counter + rcvr = offer.counter currency = offer.buy escrow_instance = get_escrow_instance(offer.escrow) answer = "{0}\n{1} sender: {2}{3}\n{1} receiver: {4}{5}\nBank: {6}\nMemo: {7}" answer = answer.format( markdown.link("Unconfirmed escrow.", escrow_instance.trx_url(offer.trx_id)), - currency, + markdown.escape_md(currency), markdown.link(sender["mention"], User(id=sender["id"]).url), - " ({})".format(sender["name"]) if "name" in sender else "", - markdown.link(receiver["mention"], User(id=receiver["id"]).url), - " ({})".format(receiver["name"]) if "name" in receiver else "", + markdown.escape_md(" ({})".format(sender["name"])) if "name" in sender else "", + markdown.link(rcvr["mention"], User(id=rcvr["id"]).url), + markdown.escape_md(" ({})".format(rcvr["name"])) if "name" in rcvr else "", offer.bank, markdown.code(offer.memo), ) - await tg.send_message(config.SUPPORT_CHAT_ID, answer, parse_mode=ParseMode.MARKDOWN) + await tg.send_message( + config.SUPPORT_CHAT_ID, answer, parse_mode=ParseMode.MARKDOWN_V2 + ) await offer.delete_document() await call.answer() await tg.send_message( diff --git a/src/handlers/order.py b/src/handlers/order.py index 4334950..80c2dc1 100644 --- a/src/handlers/order.py +++ b/src/handlers/order.py @@ -387,6 +387,7 @@ async def edit_button(call: types.CallbackQuery): keyboard = types.InlineKeyboardMarkup() unset_button = types.InlineKeyboardButton(i18n("unset"), callback_data="unset") + answer = None if field == "sum_buy": answer = i18n("send_new_buy_amount") keyboard.row(unset_button) @@ -421,8 +422,6 @@ async def edit_button(call: types.CallbackQuery): elif field == "comments": answer = i18n("send_new_comments") keyboard.row(unset_button) - else: - answer = None await call.answer() if not answer: diff --git a/src/handlers/support.py b/src/handlers/support.py index 502cd78..2d3279d 100644 --- a/src/handlers/support.py +++ b/src/handlers/support.py @@ -47,17 +47,15 @@ async def send_message_to_support(message: types.Message): Envelope emoji at the beginning is the mark of support ticket. """ - if message.from_user.username: - username = "@" + message.from_user.username - else: - username = markdown.link(message.from_user.full_name, message.from_user.url) + mention = markdown.link(message.from_user.mention, message.from_user.url) await tg.send_message( config.SUPPORT_CHAT_ID, emojize(":envelope:") - + f" #chat\\_{message.chat.id} {message.message_id}\n{username}:\n" + + markdown.escape_md(f" #chat_{message.chat.id} {message.message_id}\n") + + f"{mention}:\n" + markdown.escape_md(message.text), - parse_mode=types.ParseMode.MARKDOWN, + parse_mode=types.ParseMode.MARKDOWN_V2, ) await tg.send_message( message.chat.id, diff --git a/src/i18n.py b/src/i18n.py index 8ecb1be..a0faa36 100644 --- a/src/i18n.py +++ b/src/i18n.py @@ -14,11 +14,13 @@ # # You should have received a copy of the GNU Affero General Public License # along with TellerBot. If not, see . +import re import typing from pathlib import Path from aiogram import types from aiogram.contrib.middlewares.i18n import I18nMiddleware +from aiogram.utils import markdown from pymongo import ReturnDocument from src.database import database @@ -35,6 +37,18 @@ def __init__(self, domain, path, default="en"): self.path = path self.default = default + def __call__(self, *args, escape_md=False, **kwargs) -> str: + """Call I18nMiddleware with option to escape markdown retaining formatting.""" + translation = super().__call__(*args, **kwargs) + if escape_md: + placeholders = re.findall(r"{\w*}", translation) + text_parts = map(markdown.escape_md, re.split(r"{\w*}", translation)) + first_part = next(text_parts) + rest_generator = (p + tp for p, tp in zip(placeholders, text_parts)) + return "".join((first_part, *rest_generator)) + else: + return translation + async def get_user_locale( self, action: str, args: typing.Tuple[typing.Any] ) -> typing.Optional[str]: diff --git a/src/notifications.py b/src/notifications.py index 580d301..c080edf 100644 --- a/src/notifications.py +++ b/src/notifications.py @@ -18,6 +18,8 @@ import typing from time import time +from aiogram.types import ParseMode +from aiogram.utils import markdown from aiogram.utils.exceptions import TelegramAPIError from src.bot import tg @@ -36,12 +38,14 @@ async def run_loop(): sent = False async for order in cursor: user = await database.users.find_one({"id": order["user_id"]}) - message = i18n("order_expired", locale=user["locale"]) - message += "\nID: {}".format(order["_id"]) + message = i18n("order_expired", locale=user["locale"], escape_md=True) + message += "\nID: {}".format(markdown.code(order["_id"])) try: if sent: await asyncio.sleep(1) # Avoid Telegram limit - await tg.send_message(user["chat"], message) + await tg.send_message( + user["chat"], message, parse_mode=ParseMode.MARKDOWN_V2 + ) except TelegramAPIError: pass else: