예제 #1
0
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(),
    )
예제 #2
0
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
예제 #3
0
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
예제 #4
0
    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
예제 #5
0
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"])
예제 #6
0
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,
            }
        },
    )
예제 #7
0
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()
예제 #8
0
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,
        )
예제 #9
0
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,
        )