async def price_ask( call: types.CallbackQuery, order: Mapping[str, Any], price_currency: str ): """Edit currency of price in message to ``price_currency`` field value.""" if price_currency == "sell": answer = i18n("ask_buy_price {of_currency} {per_currency}").format( of_currency=order["sell"], per_currency=order["buy"] ) callback_command = "buy" else: answer = ( i18n("ask_sell_price {of_currency} {per_currency}").format( of_currency=order["buy"], per_currency=order["sell"] ), ) callback_command = "sell" buttons = await inline_control_buttons() callback_data = f"price {callback_command}" buttons.insert( 0, [InlineKeyboardButton(i18n("invert"), callback_data=callback_data)], ) await tg.edit_message_text( answer, call.message.chat.id, call.message.message_id, reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), )
async def choose_sell(message: types.Message, state: FSMContext): """Set currency user wants to sell and ask for price.""" if message.text.startswith(emojize(":fast_reverse_button:")): await OrderCreation.buy.set() await tg.send_message( message.chat.id, i18n("ask_buy_currency"), reply_markup=whitelist.currency_keyboard("buy"), ) return elif message.text.startswith(emojize(":x:")): await cancel_order_creation(message.from_user.id, message.chat.id) return match = await match_currency("sell", message) if not match: return order = await database.creation.find_one_and_update( {"user_id": message.from_user.id, "buy": {"$ne": match}}, {"$set": {"sell": match, "price_currency": "sell"}}, return_document=ReturnDocument.AFTER, ) if not order: await tg.send_message( message.chat.id, i18n("same_currency_error"), reply_markup=whitelist.currency_keyboard("sell"), ) return await set_price_state(message, order)
async def change_state(call: types.CallbackQuery, state: FSMContext): """React to back/skip button query.""" args = call.data.split() query_state_name = args[1] direction = args[2] state_name = await state.get_state() if state_name != query_state_name: return await call.answer(i18n("wrong_button")) if state_name in OrderCreation.all_states_names: if direction == "back": new_state = await OrderCreation.previous() elif direction == "skip": new_state = await OrderCreation.next() handler = state_handlers.get(new_state) if handler: error = await handler(call) if not error: return await call.answer() await state.set_state(state_name) if direction == "back": answer = i18n("back_error") elif direction == "skip": answer = i18n("skip_error") return await call.answer(answer)
async def delete_button(call: types.CallbackQuery, order: OrderType): """React to "Delete" button by asking user to confirm deletion.""" args = call.data.split() location_message_id = int(args[2]) show_id = call.message.text.startswith("ID") keyboard = types.InlineKeyboardMarkup() keyboard.row( types.InlineKeyboardButton( i18n("totally_sure"), callback_data="confirm_delete {} {}".format( order["_id"], location_message_id ), ) ) keyboard.row( types.InlineKeyboardButton( i18n("no"), callback_data="revert {} {} 0 {}".format( order["_id"], location_message_id, int(show_id) ), ) ) await tg.edit_message_text( i18n("delete_order_confirmation"), call.message.chat.id, call.message.message_id, reply_markup=keyboard, )
async def set_name(message: types.Message, offer: EscrowOffer): """Set fiat sender's name on card and ask for first and last 4 digits.""" name = message.text.split() if len(name) != 3: await tg.send_message( message.chat.id, i18n("wrong_word_count {word_count}").format(word_count=3), ) return name[2] = name[2][0] + "." # Leaving the first letter of surname with dot if offer.type == "buy": user_field = "counter" currency = offer.sell else: user_field = "init" currency = offer.buy await offer.update_document( {"$set": { f"{user_field}.name": " ".join(name).upper() }}) await tg.send_message( message.chat.id, i18n("send_first_and_last_4_digits_of_card_number {currency}").format( currency=currency), ) await states.Escrow.send_card_number.set()
async def claim_currency(call: types.CallbackQuery): """Set cashback currency and suggest last escrow address.""" currency = call.data.split()[1] cursor = (database.cashback.find({ "id": call.from_user.id, "currency": currency, "address": { "$ne": None } }).sort("time", pymongo.DESCENDING).limit(1)) last_cashback = await cursor.to_list(length=1) if last_cashback: address = last_cashback[0]["address"] keyboard = InlineKeyboardMarkup(row_width=1) keyboard.add( InlineKeyboardButton( i18n("confirm_cashback_address"), callback_data=f"claim_transfer {currency} {address}", ), InlineKeyboardButton( i18n("custom_cashback_address"), callback_data=f"custom_cashback_address {currency}", ), ) await call.answer() await tg.edit_message_text( i18n("use_cashback_address {address}").format( address=markdown.code(address)), call.message.chat.id, call.message.message_id, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN, ) else: return await custom_cashback_address(call)
async def whitelisting_request(call: types.CallbackQuery): """Send whitelisting request to support or increment requests count.""" currency = call.data.split()[1] request = await database.whitelisting_requests.find_one_and_update( {"_id": currency}, {"$addToSet": {"users": call.from_user.id}}, upsert=True, return_document=ReturnDocument.BEFORE, ) double_request = False if request: if call.from_user.id in request["users"]: double_request = True else: support_text = emojize(":label: #whitelisting_request {} - {}.").format( currency, len(request["users"]) + 1 ) if len(request["users"]) == 1: message = await tg.send_message(config.SUPPORT_CHAT_ID, support_text) await database.whitelisting_requests.update_one( {"_id": request["_id"]}, {"$set": {"message_id": message.message_id}}, ) else: await tg.edit_message_text( support_text, config.SUPPORT_CHAT_ID, request["message_id"], ) await call.answer() await tg.send_message( call.message.chat.id, i18n("double_request") if double_request else i18n("request_sent"), )
async def choose_duration(message: types.Message, state: FSMContext): """Set duration and ask for comments.""" try: duration = int(message.text) if duration <= 0: raise ValueError except ValueError: await tg.send_message(message.chat.id, i18n("send_natural_number")) return if duration > config.ORDER_DURATION_LIMIT: await tg.send_message( message.chat.id, i18n("exceeded_duration_limit {limit}").format( limit=config.ORDER_DURATION_LIMIT), ) return await database.creation.update_one({"user_id": message.from_user.id}, {"$set": { "duration": duration }}) await OrderCreation.comments.set() await tg.send_message( message.chat.id, i18n("ask_comments"), reply_markup=InlineKeyboardMarkup( inline_keyboard=await inline_control_buttons()), )
async def choose_bank(call: types.CallbackQuery, offer: EscrowOffer): """Set chosen bank and continue. Because bank is chosen by initiator, ask for receive address if they receive escrow asset. """ bank = call.data.split()[2] if bank not in SUPPORTED_BANKS: await call.answer(i18n("bank_not_supported")) return update_dict = {"bank": bank} await call.answer() update_dict["pending_input_from"] = call.from_user.id await offer.update_document({"$set": update_dict}) if offer.sell == "RUB": await tg.send_message( call.message.chat.id, i18n("send_first_and_last_4_digits_of_card_number {currency}"). format(currency=offer.sell), ) await states.Escrow.receive_card_number.set() else: await tg.send_message( call.message.chat.id, i18n("ask_address {currency}").format(currency=offer.sell), ) await states.Escrow.receive_address.set()
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 choose_buy_gateway(message: types.Message, state: FSMContext): """Set gateway of buy currency and ask for sell currency.""" if message.text.startswith(emojize(":fast_reverse_button:")): await OrderCreation.previous() await tg.send_message( message.chat.id, i18n("ask_buy_currency"), reply_markup=whitelist.currency_keyboard("buy"), ) return elif message.text.startswith(emojize(":x:")): await cancel_order_creation(message.from_user.id, message.chat.id) return elif not message.text.startswith(emojize(":fast_forward:")): gateway_result = await get_currency_with_gateway("buy", message) if not gateway_result: return order, currency = gateway_result await database.creation.update_one({"_id": order["_id"]}, {"$set": { "buy": currency }}) await OrderCreation.sell.set() await tg.send_message( message.chat.id, i18n("ask_sell_currency"), reply_markup=whitelist.currency_keyboard("sell"), )
async def full_card_number_sent(call: types.CallbackQuery, offer: EscrowOffer): """Confirm that full card number is sent and ask for first and last 4 digits.""" await offer.update_document( {"$set": { "pending_input_from": call.from_user.id }}) await call.answer() if call.from_user.id == offer.init["id"]: counter = offer.counter await tg.send_message( counter["id"], i18n("ask_address {currency}").format(currency=offer.buy)) await tg.send_message( call.message.chat.id, i18n("exchange_continued {user}").format( user=markdown.link(counter["mention"], User(id=counter["id"]).url)), parse_mode=ParseMode.MARKDOWN, ) await offer.update_document( {"$set": { "pending_input_from": counter["id"] }}) counter_state = FSMContext(dp.storage, counter["id"], counter["id"]) await counter_state.set_state(states.Escrow.receive_address.state) await states.Escrow.receive_card_number.set() else: await tg.send_message( call.message.chat.id, i18n("send_first_and_last_4_digits_of_card_number {currency}"). format(currency=offer.sell if offer.type == "buy" else offer.buy), ) await states.Escrow.receive_card_number.set()
async def archive_button(call: types.CallbackQuery, order: OrderType): """React to "Archive" or "Unarchive" button by flipping archived flag.""" args = call.data.split() archived = order.get("archived") if archived: update_dict = {"$unset": {"archived": True}, "$set": {"notify": True}} else: update_dict = {"$set": {"archived": True, "notify": False}} order = await database.orders.find_one_and_update( {"_id": ObjectId(args[1]), "user_id": call.from_user.id}, update_dict, return_document=pymongo.ReturnDocument.AFTER, ) if not order: await call.answer( i18n("unarchive_order_error") if archived else i18n("archive_order_error") ) return await call.answer() await show_order( order, call.message.chat.id, call.from_user.id, message_id=call.message.message_id, location_message_id=int(args[2]), show_id=call.message.text.startswith("ID"), )
async def handle_start_command(message: types.Message, state: FSMContext): """Handle /start. Ask for language if user is new or show menu. """ user = {"id": message.from_user.id, "chat": message.chat.id} result = await database.users.update_one(user, {"$setOnInsert": user}, upsert=True) if not result.matched_count: keyboard = InlineKeyboardMarkup() for language in i18n.available_locales: keyboard.row( InlineKeyboardButton( Locale(language).display_name, callback_data="locale {}".format(language), ) ) await tg.send_message( message.chat.id, i18n("choose_language"), reply_markup=keyboard ) return await state.finish() await tg.send_message( message.chat.id, i18n("help_message"), reply_markup=start_keyboard() )
async def handle_create(message: types.Message, state: FSMContext): """Start order creation by asking user for currency they want to buy.""" current_time = time() user_orders = await database.orders.count_documents( { "user_id": message.from_user.id, "start_time": {"$gt": current_time - config.ORDERS_LIMIT_HOURS * 3600}, } ) if user_orders >= config.ORDERS_LIMIT_COUNT: await tg.send_message( message.chat.id, i18n("exceeded_order_creation_time_limit {orders} {hours}").format( orders=config.ORDERS_LIMIT_COUNT, hours=config.ORDERS_LIMIT_HOURS ), ) return creation = {"user_id": message.from_user.id} await database.creation.find_one_and_replace(creation, creation, upsert=True) await states.OrderCreation.first() await tg.send_message( message.chat.id, i18n("ask_buy_currency"), reply_markup=whitelist.currency_keyboard("buy"), )
async def sum_handler(call: types.CallbackQuery): """Ask for sum currency.""" order = await database.creation.find_one_and_update( {"user_id": call.from_user.id}, {"$unset": { "price_currency": True }}) if not order: await call.answer(i18n("no_creation")) return True keyboard = InlineKeyboardMarkup() keyboard.add( InlineKeyboardButton(order["buy"], callback_data="sum buy"), InlineKeyboardButton(order["sell"], callback_data="sum sell"), ) for row in await inline_control_buttons(): keyboard.row(*row) await tg.edit_message_text( i18n("ask_sum_currency"), call.message.chat.id, call.message.message_id, reply_markup=keyboard, )
async def search_by_creator(message: types.Message, state: FSMContext): """Search orders by creator. Creator is indicated with username (with or without @) or user ID after **/creator** or **/c** in message text. In contrast to usernames and user IDs, names aren't unique and therefore not supported. """ query: typing.Dict[str, typing.Any] = { "$or": [{ "archived": { "$exists": False } }, { "archived": False }], "expiration_time": { "$gt": time() }, } source = message.text.split() try: creator = source[1] if creator.isdigit(): query["user_id"] = int(creator) else: mention_regexp = f"^{creator}$" if creator[ 0] == "@" else f"^@{creator}$" user = await database.users.find_one({ "mention": re.compile(mention_regexp, re.IGNORECASE), "has_username": True, }) if user: query["user_id"] = user["id"] else: await tg.send_message(message.chat.id, i18n("user_not_found")) return except IndexError: await tg.send_message( message.chat.id, i18n("no_user_argument"), ) return cursor = database.orders.find(query).sort("start_time", DESCENDING) quantity = await database.orders.count_documents(query) await state.finish() await orders_list(cursor, message.chat.id, 0, quantity, "orders", user_id=message.from_user.id)
async def choose_sell_gateway(message: types.Message, state: FSMContext): """Set gateway of sell currency and ask for price.""" if message.text.startswith(emojize(":fast_reverse_button:")): await OrderCreation.previous() await tg.send_message( message.chat.id, i18n("ask_sell_currency"), reply_markup=whitelist.currency_keyboard("sell"), ) return elif message.text.startswith(emojize(":x:")): await cancel_order_creation(message.from_user.id, message.chat.id) return same_gateway = False if not message.text.startswith(emojize(":fast_forward:")): gateway_result = await get_currency_with_gateway("sell", message) if not gateway_result: return order, currency = gateway_result if currency == order["buy"]: same_gateway = True else: await database.creation.update_one( {"_id": order["_id"]}, {"$set": { "sell": currency, "price_currency": "sell" }}, ) else: order = await database.creation.find_one_and_update( { "user_id": message.from_user.id, "$expr": { "$ne": ["$buy", "$sell"] } }, {"$set": { "price_currency": "sell" }}, return_document=ReturnDocument.AFTER, ) if not order: order = await database.creation.find_one( {"user_id": message.from_user.id}) same_gateway = True if same_gateway: await tg.send_message( message.chat.id, i18n("same_gateway_error"), reply_markup=whitelist.gateway_keyboard(order["sell"], "sell"), ) else: await set_price_state(message, order)
async def help_command(message: types.Message): """Handle request to support.""" await states.asking_support.set() await tg.send_message( message.chat.id, i18n("request_question"), reply_markup=InlineKeyboardMarkup(inline_keyboard=[[ InlineKeyboardButton(i18n("cancel"), callback_data="unhelp") ]]), )
async def decline_offer(call: types.CallbackQuery, offer: EscrowOffer): """React to counteragent declining offer.""" offer.react_time = time() await offer.delete_document() await tg.send_message( offer.init["id"], i18n("escrow_offer_declined", locale=offer.init["locale"]), ) await call.answer() await tg.send_message(call.message.chat.id, i18n("offer_declined"))
def gateway_keyboard(currency: str, currency_type: str) -> ReplyKeyboardMarkup: """Get keyboard with gateways of ``currency`` from whitelist.""" keyboard = ReplyKeyboardMarkup(row_width=5, one_time_keyboard=currency_type == "sell") keyboard.add(*[KeyboardButton(g) for g in CRYPTOCURRENCY[currency]]) keyboard.row( KeyboardButton(emojize(":fast_reverse_button: ") + i18n("back")), KeyboardButton(emojize(":fast_forward: ") + i18n("without_gateway")), KeyboardButton(emojize(":x: ") + i18n("cancel")), ) return keyboard
async def toggle_escrow(message: types.Message): """Toggle escrow availability. This command makes creation of new escrow offers unavailable if escrow is enabled, and makes it available if it's disabled. """ config.ESCROW_ENABLED = not config.ESCROW_ENABLED if config.ESCROW_ENABLED: await tg.send_message(message.chat.id, i18n("escrow_enabled")) else: await tg.send_message(message.chat.id, i18n("escrow_disabled"))
async def set_price_state(message: types.Message, order: Mapping[str, Any]): """Ask for price.""" await OrderCreation.price.set() buttons = await inline_control_buttons(back=False) buttons.insert( 0, [InlineKeyboardButton(i18n("invert"), callback_data="price buy")]) await tg.send_message( message.chat.id, i18n("ask_buy_price {of_currency} {per_currency}").format( of_currency=order["sell"], per_currency=order["buy"]), reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), )
async def set_escrow_sum(message: types.Message, offer: EscrowOffer): """Set sum and ask for fee payment agreement.""" try: offer_sum = money(message.text) except MoneyValueError as exception: await tg.send_message(message.chat.id, str(exception)) return order = await database.orders.find_one({"_id": offer.order}) order_sum = order.get(offer.sum_currency) if order_sum and offer_sum > order_sum.to_decimal(): await tg.send_message(message.chat.id, i18n("exceeded_order_sum")) return update_dict = {offer.sum_currency: Decimal128(offer_sum)} new_currency = "sell" if offer.sum_currency == "sum_buy" else "buy" update_dict[f"sum_{new_currency}"] = Decimal128( normalize(offer_sum * order[f"price_{new_currency}"].to_decimal())) escrow_sum = update_dict[f"sum_{offer.type}"] escrow_fee = Decimal(config.ESCROW_FEE_PERCENTS) / Decimal("100") update_dict["sum_fee_up"] = Decimal128( normalize(escrow_sum.to_decimal() * (Decimal("1") + escrow_fee))) update_dict["sum_fee_down"] = Decimal128( normalize(escrow_sum.to_decimal() * (Decimal("1") - escrow_fee))) offer = replace(offer, **update_dict) # type: ignore if offer.sum_currency == offer.type: insured = await get_insurance(offer) update_dict["insured"] = Decimal128(insured) if offer_sum > insured: keyboard = InlineKeyboardMarkup() keyboard.add( InlineKeyboardButton( i18n("continue"), callback_data=f"accept_insurance {offer._id}"), InlineKeyboardButton(i18n("cancel"), callback_data=f"init_cancel {offer._id}"), ) answer = i18n("exceeded_insurance {amount} {currency}").format( amount=insured, currency=offer.escrow) answer += "\n" + i18n("exceeded_insurance_options") await tg.send_message(message.chat.id, answer, reply_markup=keyboard) else: await ask_fee(message.from_user.id, message.chat.id, offer) await offer.update_document({ "$set": update_dict, "$unset": { "sum_currency": True } })
async def final_offer_confirmation(call: types.CallbackQuery, offer: EscrowOffer): """Ask not escrow asset receiver to confirm transfer.""" if not offer.unsent: await call.answer(i18n("transfer_already_confirmed")) return await offer.update_document({"$unset": {"unsent": True}}) if offer.type == "buy": confirm_user = offer.init other_user = offer.counter currency = offer.sell elif offer.type == "sell": confirm_user = offer.counter other_user = offer.init currency = offer.buy keyboard = InlineKeyboardMarkup() keyboard.add( InlineKeyboardButton( i18n("yes", locale=confirm_user["locale"]), callback_data=f"escrow_complete {offer._id}", )) reply = await tg.send_message( confirm_user["id"], i18n("receiving_confirmation {currency} {user}", locale=confirm_user["locale"]).format( currency=currency, user=other_user["send_address"]), reply_markup=keyboard, ) keyboard.add( InlineKeyboardButton( i18n("no", locale=confirm_user["locale"]), callback_data=f"escrow_validate {offer._id}", )) await call_later( 60 * 10, edit_keyboard, offer._id, confirm_user["id"], reply.message_id, keyboard, ) await call.answer() await tg.send_message( other_user["id"], i18n( "complete_escrow_promise", locale=other_user["locale"], ), reply_markup=start_keyboard(), )
def start_keyboard() -> types.ReplyKeyboardMarkup: """Create reply keyboard with main menu.""" keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True, row_width=2) keyboard.add( types.KeyboardButton( emojize(":heavy_plus_sign: ") + i18n("create_order")), types.KeyboardButton( emojize(":bust_in_silhouette: ") + i18n("my_orders")), types.KeyboardButton(emojize(":closed_book: ") + i18n("order_book")), types.KeyboardButton(emojize(":abcd: ") + i18n("language")), types.KeyboardButton(emojize(":question: ") + i18n("support")), ) return keyboard
async def cancel_order_creation(user_id: int, chat_id: int): """Cancel order creation.""" await dp.get_current().current_state().finish() order = await database.creation.delete_one({"user_id": user_id}) if not order.deleted_count: await tg.send_message( chat_id, i18n("no_creation"), reply_markup=start_keyboard(), ) return True await tg.send_message( chat_id, i18n("order_cancelled"), reply_markup=start_keyboard() )
def currency_keyboard(currency_type: str) -> ReplyKeyboardMarkup: """Get keyboard with currencies from whitelists.""" keyboard = ReplyKeyboardMarkup(row_width=5, one_time_keyboard=currency_type == "sell") keyboard.row(*[KeyboardButton(c) for c in FIAT]) keyboard.add(*[KeyboardButton(c) for c in CRYPTOCURRENCY]) cancel_button = KeyboardButton(emojize(":x: ") + i18n("cancel")) if currency_type == "sell": keyboard.row( KeyboardButton(emojize(":fast_reverse_button: ") + i18n("back")), cancel_button, ) else: keyboard.row(cancel_button) return keyboard
async def set_receive_card_number(message: types.Message, offer: EscrowOffer): """Create address from first and last 4 digits of card number and ask send address. First and last 4 digits of card number are sent by fiat receiver, so their send address is escrow asset address. """ card_number = await get_card_number(message.text, message.chat.id) if not card_number: return if message.from_user.id == offer.init["id"]: user_field = "init" else: user_field = "counter" await offer.update_document({ "$set": { f"{user_field}.receive_address": ("*" * 8).join(card_number) } }) await tg.send_message( message.chat.id, i18n("ask_address {currency}").format(currency=offer.escrow), ) await states.Escrow.send_address.set()
async def get_card_number( text: str, chat_id: int) -> typing.Optional[typing.Tuple[str, str]]: """Parse first and last 4 digits from card number in ``text``. If parsing is unsuccessful, send warning to ``chat_id`` and return None. Otherwise return tuple of first and last 4 digits of card number. """ if len(text) < 8: await tg.send_message(chat_id, i18n("send_at_least_8_digits")) return None first = text[:4] last = text[-4:] if not first.isdigit() or not last.isdigit(): await tg.send_message(chat_id, i18n("digits_parsing_error")) return None return (first, last)