async def get_referral_link(message: types.Message): """Send user's referral link and generate if it doesn't exist.""" user = database_user.get() code = user.get("referral_code") if code is None: while True: cryptogen = SystemRandom() code = "".join(cryptogen.choice(ascii_lowercase) for _ in range(7)) try: await database.users.update_one( {"_id": user["_id"]}, {"$set": { "referral_code": code }}) except DuplicateKeyError: continue else: break me = await tg.me answer = i18n("referral_share {link}").format( link=f"https://t.me/{me.username}?start={code}") if message.from_user.username: answer += "\n" + i18n("referral_share_alias {link}").format( link= f"https://t.me/{me.username}?start=_{message.from_user.username}") await tg.send_message( message.chat.id, answer, disable_web_page_preview=True, reply_markup=start_keyboard(), )
async def unset_button(call: types.CallbackQuery, state: FSMContext): """React to "Unset" button by unsetting the edit field.""" user = database_user.get() field = user["edit"]["field"] if field == "price": unset_dict = {"price_buy": True, "price_sell": True} else: unset_dict = {field: True} await call.answer() await finish_edit(user, {"$unset": unset_dict}) try: await tg.delete_message(user["chat"], user["edit"]["message_id"]) except MessageCantBeDeleted: return
async def default_duration(call: types.CallbackQuery, state: FSMContext): """Repeat default duration.""" user = database_user.get() order = await database.orders.find_one({"_id": user["edit"]["order_id"]}) await call.answer() await finish_edit( user, { "$set": { "expiration_time": time() + order["duration"] * 24 * 60 * 60, "notify": True, } }, ) try: await tg.delete_message(user["chat"], user["edit"]["message_id"]) except MessageCantBeDeleted: return
async def get_user_locale( self, action: str, args: typing.Tuple[typing.Any]) -> typing.Optional[str]: """Get user locale by querying collection of users in database. Return value of ``locale`` field in user's corresponding document if it exists, otherwise return user's Telegram language if possible. """ if action not in ("pre_process_message", "pre_process_callback_query"): return None user: types.User = types.User.get_current() document = database_user.get() if document: locale = document.get("locale", user.language_code) else: locale = user.language_code return locale if locale in self.available_locales else self.default
async def edit_field(message: types.Message, state: FSMContext): """Ask new value of chosen order's field during editing.""" user = database_user.get() edit = user["edit"] field = edit["field"] invert = user.get("invert_order", False) set_dict = {} error = None if field == "sum_buy": try: transaction_sum = money.money(message.text) except money.MoneyValueError as exception: error = str(exception) else: order = await database.orders.find_one({"_id": edit["order_id"]}) set_dict["sum_buy"] = Decimal128(transaction_sum) if "price_sell" in order: set_dict["sum_sell"] = Decimal128( money.normalize(transaction_sum * order["price_sell"].to_decimal()) ) elif field == "sum_sell": try: transaction_sum = money.money(message.text) except money.MoneyValueError as exception: error = str(exception) else: order = await database.orders.find_one({"_id": edit["order_id"]}) set_dict["sum_sell"] = Decimal128(transaction_sum) if "price_buy" in order: set_dict["sum_buy"] = Decimal128( money.normalize(transaction_sum * order["price_buy"].to_decimal()) ) elif field == "price": try: price = money.money(message.text) except money.MoneyValueError as exception: error = str(exception) else: order = await database.orders.find_one({"_id": edit["order_id"]}) if invert: price_sell = money.normalize(Decimal(1) / price) set_dict["price_buy"] = Decimal128(price) set_dict["price_sell"] = Decimal128(price_sell) if order.get("sum_currency") == "buy": set_dict["sum_sell"] = Decimal128( money.normalize(order["sum_buy"].to_decimal() * price_sell) ) elif "sum_sell" in order: set_dict["sum_buy"] = Decimal128( money.normalize(order["sum_sell"].to_decimal() * price) ) else: price_buy = money.normalize(Decimal(1) / price) set_dict["price_buy"] = Decimal128(price_buy) set_dict["price_sell"] = Decimal128(price) if order.get("sum_currency") == "sell": set_dict["sum_buy"] = Decimal128( money.normalize(order["sum_sell"].to_decimal() * price_buy) ) elif "sum_buy" in order: set_dict["sum_sell"] = Decimal128( money.normalize(order["sum_buy"].to_decimal() * price) ) elif field == "payment_system": payment_system = message.text.replace("\n", " ") if len(payment_system) >= 150: await tg.send_message( message.chat.id, i18n("exceeded_character_limit {limit} {sent}").format( limit=150, sent=len(payment_system) ), ) return set_dict["payment_system"] = payment_system elif field == "duration": try: duration = int(message.text) if duration <= 0: raise ValueError except ValueError: error = i18n("send_natural_number") else: if duration > config.ORDER_DURATION_LIMIT: error = i18n("exceeded_duration_limit {limit}").format( limit=config.ORDER_DURATION_LIMIT ) else: order = await database.orders.find_one({"_id": edit["order_id"]}) set_dict["duration"] = duration set_dict["expiration_time"] = time() + duration * 24 * 60 * 60 set_dict["notify"] = True elif field == "comments": comments = message.text if len(comments) >= 150: await tg.send_message( message.chat.id, i18n("exceeded_character_limit {limit} {sent}").format( limit=150, sent=len(comments) ), ) return set_dict["comments"] = comments if set_dict: await finish_edit(user, {"$set": set_dict}) await message.delete() try: await tg.delete_message(user["chat"], edit["message_id"]) except MessageCantBeDeleted: return elif error: await message.delete() await tg.edit_message_text(error, message.chat.id, edit["message_id"])
async def edit_button(call: types.CallbackQuery): """React to "Edit" button by entering edit mode on order.""" args = call.data.split() order_id = args[1] order = await database.orders.find_one( {"_id": ObjectId(order_id), "user_id": call.from_user.id} ) if not order: await call.answer(i18n("edit_order_error")) return field = args[2] keyboard = types.InlineKeyboardMarkup() unset_button = types.InlineKeyboardButton(i18n("unset"), callback_data="unset") if field == "sum_buy": answer = i18n("send_new_buy_amount") keyboard.row(unset_button) elif field == "sum_sell": answer = i18n("send_new_sell_amount") keyboard.row(unset_button) elif field == "price": user = database_user.get() answer = i18n("new_price {of_currency} {per_currency}") if user.get("invert_order", False): answer = answer.format(of_currency=order["buy"], per_currency=order["sell"]) else: answer = answer.format(of_currency=order["sell"], per_currency=order["buy"]) keyboard.row(unset_button) elif field == "payment_system": answer = i18n("send_new_payment_system") keyboard.row(unset_button) elif field == "duration": answer = i18n("send_new_duration {limit}").format( limit=config.ORDER_DURATION_LIMIT ) keyboard.row( types.InlineKeyboardButton( plural_i18n( "repeat_duration_singular {days}", "repeat_duration_plural {days}", order["duration"], ).format(days=order["duration"]), callback_data="default_duration", ) ) elif field == "comments": answer = i18n("send_new_comments") keyboard.row(unset_button) else: answer = None await call.answer() if not answer: return user = database_user.get() if "edit" in user: await tg.delete_message(call.message.chat.id, user["edit"]["message_id"]) result = await tg.send_message(call.message.chat.id, answer, reply_markup=keyboard,) await database.users.update_one( {"_id": user["_id"]}, { "$set": { "edit.order_message_id": call.message.message_id, "edit.message_id": result.message_id, "edit.order_id": order["_id"], "edit.field": field, "edit.location_message_id": int(args[3]), "edit.one_time": bool(int(args[4])), "edit.show_id": call.message.text.startswith("ID"), STATE_KEY: states.field_editing.state, } }, )
async def escrow_button(call: types.CallbackQuery, order: OrderType): """React to "Escrow" button by starting escrow exchange.""" if not config.ESCROW_ENABLED: await call.answer(i18n("escrow_unavailable")) return args = call.data.split() currency_arg = args[2] edit = bool(int(args[3])) if currency_arg == "sum_buy": sum_currency = order["buy"] new_currency = order["sell"] new_currency_arg = "sum_sell" elif currency_arg == "sum_sell": sum_currency = order["sell"] new_currency = order["buy"] new_currency_arg = "sum_buy" else: return keyboard = types.InlineKeyboardMarkup() keyboard.row( types.InlineKeyboardButton( i18n("change_to {currency}").format(currency=new_currency), callback_data="escrow {} {} 1".format(order["_id"], new_currency_arg), ) ) answer = i18n("send_exchange_sum {currency}").format(currency=sum_currency) if edit: cancel_data = call.message.reply_markup.inline_keyboard[1][0].callback_data keyboard.row( types.InlineKeyboardButton(i18n("cancel"), callback_data=cancel_data) ) await database.escrow.update_one( {"pending_input_from": call.from_user.id}, {"$set": {"sum_currency": currency_arg}}, ) await call.answer() await tg.edit_message_text( answer, call.message.chat.id, call.message.message_id, reply_markup=keyboard ) else: offer_id = ObjectId() keyboard.row( types.InlineKeyboardButton( i18n("cancel"), callback_data=f"init_cancel {offer_id}" ) ) escrow_type = "buy" if get_escrow_instance(order["buy"]) else "sell" user = database_user.get() init_user = { "id": user["id"], "locale": user["locale"], "mention": user["mention"], } counter_user = await database.users.find_one( {"id": order["user_id"]}, projection={"id": True, "locale": True, "mention": True}, ) await database.escrow.delete_many({"init.send_address": {"$exists": False}}) offer = EscrowOffer( **{ "_id": offer_id, "order": order["_id"], "buy": order["buy"], "sell": order["sell"], "type": escrow_type, "escrow": order[escrow_type], "time": time(), "sum_currency": currency_arg, "init": init_user, "counter": counter_user, "pending_input_from": call.message.chat.id, } ) await offer.insert_document() await call.answer() await tg.send_message(call.message.chat.id, answer, reply_markup=keyboard) await states.Escrow.amount.set()
async def orders_list( cursor: Cursor, chat_id: int, start: int, quantity: int, buttons_data: str, user_id: typing.Optional[int] = None, message_id: typing.Optional[int] = None, invert: typing.Optional[bool] = None, ) -> None: """Send list of orders. :param cursor: Cursor of MongoDB query to orders. :param chat_id: Telegram ID of current chat. :param start: Start index. :param quantity: Quantity of orders in cursor. :param buttons_data: Beginning of callback data of left/right buttons. :param user_id: Telegram ID of current user if cursor is not user-specific. :param message_id: Telegram ID of message to edit. :param invert: Invert all prices. """ user = database_user.get() if invert is None: invert = user.get("invert_book", False) else: await database.users.update_one({"_id": user["_id"]}, {"$set": { "invert_book": invert }}) keyboard = types.InlineKeyboardMarkup( row_width=min(config.ORDERS_COUNT // 2, 8)) inline_orders_buttons = ( types.InlineKeyboardButton( emojize(":arrow_left:"), callback_data="{} {} {}".format(buttons_data, start - config.ORDERS_COUNT, 1 if invert else 0), ), types.InlineKeyboardButton( emojize(":arrow_right:"), callback_data="{} {} {}".format(buttons_data, start + config.ORDERS_COUNT, 1 if invert else 0), ), ) if quantity == 0: keyboard.row(*inline_orders_buttons) text = i18n("no_orders") if message_id is None: await tg.send_message(chat_id, text, reply_markup=keyboard) else: await tg.edit_message_text(text, chat_id, message_id, reply_markup=keyboard) return all_orders = await cursor.to_list(length=start + config.ORDERS_COUNT) orders = all_orders[start:] lines = [] buttons = [] current_time = time() for i, order in enumerate(orders): line = "" if user_id is None: if not order.get( "archived") and order["expiration_time"] > current_time: line += emojize(":arrow_forward: ") else: line += emojize(":pause_button: ") exp = Decimal("1e-5") if "sum_sell" in order: line += "{:,} ".format( normalize(order["sum_sell"].to_decimal(), exp)) line += "{} → ".format(order["sell"]) if "sum_buy" in order: line += "{:,} ".format( normalize(order["sum_buy"].to_decimal(), exp)) line += order["buy"] if "price_sell" in order: if invert: line += " ({:,} {}/{})".format( normalize(order["price_buy"].to_decimal(), exp), order["buy"], order["sell"], ) else: line += " ({:,} {}/{})".format( normalize(order["price_sell"].to_decimal(), exp), order["sell"], order["buy"], ) if user_id is not None and order["user_id"] == user_id: line = f"*{line}*" lines.append(f"{i + 1}. {line}") buttons.append( types.InlineKeyboardButton("{}".format(i + 1), callback_data="get_order {}".format( order["_id"]))) keyboard.row( types.InlineKeyboardButton( i18n("invert"), callback_data="{} {} {}".format(buttons_data, start, int(not invert)), )) keyboard.add(*buttons) keyboard.row(*inline_orders_buttons) text = ("\\[" + i18n("page {number} {total}").format( number=math.ceil(start / config.ORDERS_COUNT) + 1, total=math.ceil(quantity / config.ORDERS_COUNT), ) + "]\n" + "\n".join(lines)) if message_id is None: await tg.send_message( chat_id, text, reply_markup=keyboard, parse_mode=types.ParseMode.MARKDOWN, disable_web_page_preview=True, ) else: await tg.edit_message_text( text, chat_id, message_id, reply_markup=keyboard, parse_mode=types.ParseMode.MARKDOWN, disable_web_page_preview=True, )
async def show_order( order: typing.Mapping[str, typing.Any], chat_id: int, user_id: int, message_id: typing.Optional[int] = None, location_message_id: typing.Optional[int] = None, show_id: bool = False, invert: typing.Optional[bool] = None, edit: bool = False, locale: typing.Optional[str] = None, ): """Send detailed order. :param order: Order document. :param chat_id: Telegram ID of chat to send message to. :param user_id: Telegram user ID of message receiver. :param message_id: Telegram ID of message to edit. :param location_message_id: Telegram ID of message with location object. It is deleted when **Hide** inline button is pressed. :param show_id: Add ID of order to the top. :param invert: Invert price. :param edit: Enter edit mode. :param locale: Locale of message receiver. """ if locale is None: locale = i18n.ctx_locale.get() new_edit_msg = None if invert is None: try: user = database_user.get() except LookupError: user = await database.users.find_one({"id": user_id}) invert = user.get("invert_order", False) else: user = await database.users.find_one_and_update( {"id": user_id}, {"$set": { "invert_order": invert }}) if "edit" in user: if edit: if user["edit"]["field"] == "price": new_edit_msg = i18n( "new_price {of_currency} {per_currency}", locale=locale) if invert: new_edit_msg = new_edit_msg.format( of_currency=order["buy"], per_currency=order["sell"]) else: new_edit_msg = new_edit_msg.format( of_currency=order["sell"], per_currency=order["buy"]) elif user["edit"]["order_message_id"] == message_id: await tg.delete_message(user["chat"], user["edit"]["message_id"]) await database.users.update_one( {"_id": user["_id"]}, {"$unset": { "edit": True, STATE_KEY: True }}) if location_message_id is None: if order.get("lat") is not None and order.get("lon") is not None: location_message = await tg.send_location(chat_id, order["lat"], order["lon"]) location_message_id = location_message.message_id else: location_message_id = -1 header = "" if show_id: header += "ID: {}\n".format(markdown.code(order["_id"])) if order.get("archived"): header += markdown.bold(i18n("archived", locale=locale)) + "\n" creator = await database.users.find_one({"id": order["user_id"]}) header += "{} ({}) ".format( markdown.link(creator["mention"], types.User(id=creator["id"]).url), markdown.code(creator["id"]), ) if invert: 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" lines = [header] field_names = { "sum_buy": i18n("buy_amount", locale=locale), "sum_sell": i18n("sell_amount", locale=locale), "price": i18n("price", locale=locale), "payment_system": i18n("payment_system", locale=locale), "duration": i18n("duration", locale=locale), "comments": i18n("comments", locale=locale), } lines_format: typing.Dict[str, typing.Optional[str]] = {} for name in field_names: lines_format[name] = None if "sum_buy" in order: lines_format["sum_buy"] = "{} {}".format(order["sum_buy"], order["buy"]) if "sum_sell" in order: lines_format["sum_sell"] = "{} {}".format(order["sum_sell"], order["sell"]) if "price_sell" in order: if invert: lines_format["price"] = "{} {}/{}".format(order["price_buy"], order["buy"], order["sell"]) else: lines_format["price"] = "{} {}/{}".format(order["price_sell"], order["sell"], order["buy"]) if "payment_system" in order: lines_format["payment_system"] = order["payment_system"] if "duration" in order: lines_format["duration"] = "{} - {}".format( datetime.utcfromtimestamp( order["start_time"]).strftime("%d.%m.%Y"), datetime.utcfromtimestamp( order["expiration_time"]).strftime("%d.%m.%Y"), ) if "comments" in order: lines_format["comments"] = "«{}»".format(order["comments"]) keyboard = types.InlineKeyboardMarkup(row_width=6) keyboard.row( types.InlineKeyboardButton( i18n("invert", locale=locale), callback_data="{} {} {} {}".format( "revert" if invert else "invert", order["_id"], location_message_id, int(edit), ), )) if edit and creator["id"] == user_id: buttons = [] for i, (field, value) in enumerate(lines_format.items()): if value is not None: lines.append(f"{i + 1}. {field_names[field]} {value}") else: lines.append(f"{i + 1}. {field_names[field]} -") buttons.append( types.InlineKeyboardButton( f"{i + 1}", callback_data="edit {} {} {} 0".format( order["_id"], field, location_message_id), )) keyboard.add(*buttons) keyboard.row( types.InlineKeyboardButton( i18n("finish", locale=locale), callback_data="{} {} {} 0".format( "invert" if invert else "revert", order["_id"], location_message_id), )) else: for field, value in lines_format.items(): if value is not None: lines.append(field_names[field] + " " + value) keyboard.row( types.InlineKeyboardButton( i18n("similar", locale=locale), callback_data="similar {}".format(order["_id"]), ), types.InlineKeyboardButton( i18n("match", locale=locale), callback_data="match {}".format(order["_id"]), ), ) if creator["id"] == user_id: keyboard.row( types.InlineKeyboardButton( i18n("edit", locale=locale), callback_data="{} {} {} 1".format( "invert" if invert else "revert", order["_id"], location_message_id, ), ), types.InlineKeyboardButton( i18n("delete", locale=locale), callback_data="delete {} {}".format( order["_id"], location_message_id), ), ) keyboard.row( types.InlineKeyboardButton( i18n("unarchive", locale=locale) if order.get("archived") else i18n("archive", locale=locale), callback_data="archive {} {}".format( order["_id"], location_message_id), ), types.InlineKeyboardButton( i18n("change_duration", locale=locale), callback_data="edit {} duration {} 1".format( order["_id"], location_message_id), ), ) elif "price_sell" in order and not order.get("archived"): if (get_escrow_instance(order["buy"]) is not None or get_escrow_instance(order["sell"]) is not None): keyboard.row( types.InlineKeyboardButton( i18n("escrow", locale=locale), callback_data="escrow {} sum_buy 0".format( order["_id"]), )) keyboard.row( types.InlineKeyboardButton( i18n("hide", locale=locale), callback_data="hide {}".format(location_message_id), )) answer = "\n".join(lines) if message_id is not None: await tg.edit_message_text( answer, chat_id, message_id, reply_markup=keyboard, parse_mode=types.ParseMode.MARKDOWN, disable_web_page_preview=True, ) if new_edit_msg is not None: keyboard = types.InlineKeyboardMarkup() keyboard.row( types.InlineKeyboardButton(i18n("unset", locale=locale), callback_data="unset")) await tg.edit_message_text( new_edit_msg, chat_id, user["edit"]["message_id"], reply_markup=keyboard, ) else: await tg.send_message( chat_id, answer, reply_markup=keyboard, parse_mode=types.ParseMode.MARKDOWN, disable_web_page_preview=True, )