From 9425af3691f982d340c51d9cc939214b78226419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D1=96=D0=BD=D0=B0=20=D0=9A=D0=BE=D0=B2=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0?= Date: Fri, 5 Sep 2025 01:59:28 +0300 Subject: [PATCH 1/5] =?UTF-8?q?fix(tg-bot):=20ack=20callback=20for=20/prof?= =?UTF-8?q?ile=20and=20"=D0=95=D1=89=D0=B5";=20harden=20adv=20profile=20pa?= =?UTF-8?q?rsing=20and=20currency;=20avoid=20extra=20account.get()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tg_bot/bot.py | 7 ++-- tg_bot/utils.py | 88 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/tg_bot/bot.py b/tg_bot/bot.py index d08faee..b43b67a 100644 --- a/tg_bot/bot.py +++ b/tg_bot/bot.py @@ -320,13 +320,14 @@ def send_profile(self, m: Message): except: self.bot.edit_message_text(_("profile_updating_error"), new_msg.chat.id, new_msg.id) logger.debug("TRACEBACK", exc_info=True) - self.bot.answer_callback_query(m.id) return def update_profile(self, c: CallbackQuery): """ Обновляет статистику аккаунта. """ + # Подтверждаем callback сразу, чтобы убрать спиннер у пользователя + self.bot.answer_callback_query(c.id) new_msg = self.bot.send_message(c.message.chat.id, _("updating_profile")) try: self.vertex.account.get() @@ -341,7 +342,6 @@ def update_profile(self, c: CallbackQuery): except: self.bot.edit_message_text(_("profile_updating_error"), new_msg.chat.id, new_msg.id) logger.debug("TRACEBACK", exc_info=True) - self.bot.answer_callback_query(c.id) return def change_cookie(self, m: telebot.types.Message): @@ -361,6 +361,8 @@ def update_adv_profile(self, c: CallbackQuery): """ Обновляет дополнительную статистику аккаунта. """ + # Подтверждаем callback сразу, чтобы убрать спиннер у пользователя + self.bot.answer_callback_query(c.id) new_msg = self.bot.send_message(c.message.chat.id, _("updating_profile")) try: self.vertex.account.get() @@ -375,7 +377,6 @@ def update_adv_profile(self, c: CallbackQuery): except: self.bot.edit_message_text(_("profile_updating_error"), new_msg.chat.id, new_msg.id) logger.debug("TRACEBACK", exc_info=True) - self.bot.answer_callback_query(c.id) return def send_orders(self, m: telebot.types.Message): diff --git a/tg_bot/utils.py b/tg_bot/utils.py index 2f0dc36..f417a54 100644 --- a/tg_bot/utils.py +++ b/tg_bot/utils.py @@ -321,20 +321,23 @@ def get_sales(account: Account, start_from: str | None = None, include_paid: boo return next_order_id, sells def generate_adv_profile(vertex: Vertex) -> str: + """ + Генерирует расширенную статистику профиля. + Защита от ошибок парсинга: при сбое возвращает частичные данные без падения. + """ account = vertex.account balance = vertex.balance - if balance.total_eur != 0: - currency = "€" - balance.balance.total_eur - elif balance.total_eur != 0: - currency = "$" - balance.balance.total_eur - elif balance.total_eur != 0: - currency = "₽" - balance.balance.total_eur + + # Выбираем валюту и базовое числовое значение из объекта баланса + # (без сетевых запросов и без перезаписи переменной balance) + if balance.total_eur: + currency, balance_value = "€", balance.total_eur + elif balance.total_usd: + currency, balance_value = "$", balance.total_usd + elif balance.total_rub: + currency, balance_value = "₽", balance.total_rub else: - balance = 0 - currency = "₽" + currency, balance_value = "₽", 0.0 if exists("storage/cache/advProfileStat.json"): with open("storage/cache/advProfileStat.json", "r", encoding="utf-8") as f: global ORDER_CONFIRMED @@ -345,7 +348,7 @@ def generate_adv_profile(vertex: Vertex) -> str: refundsPrice = {"day": 0.0, "week": 0.0, "month": 0.0, "all": 0.0} canWithdraw = {"now": 0.0, "hour": 0.0, "day": 0.0, "2day": 0.0} - account.get() + # Лишний сетевой запрос не выполняем: данные обновляются перед вызовом for order in ORDER_CONFIRMED.copy(): if time.time() - ORDER_CONFIRMED[order]["time"] > 172800: @@ -358,24 +361,47 @@ def generate_adv_profile(vertex: Vertex) -> str: else: canWithdraw["2day"] += ORDER_CONFIRMED[order]["price"] - randomLotPageLink = bs(account.method("get", "https://funpay.com/lots/693/", {}, {}).text, "html.parser").find("a", {"class": "tc-item"})["href"] - randomLotPageParse = bs(account.method("get", randomLotPageLink, {}, {}).text, "html.parser") - - balance = randomLotPageParse.select_one(".badge-balance").text.split(" ")[0] - currency = randomLotPageParse.select_one(".badge-balance").text.split(" ")[1] - - canWithdraw["now"] = randomLotPageParse.find("select", {"class": "form-control input-lg selectpicker"})["data-balance-rub"] - if currency == "$": - canWithdraw["now"] = randomLotPageParse.find("select", {"class": "form-control input-lg selectpicker"})["data-balance-usd"] - elif currency == "€": - canWithdraw["now"] = randomLotPageParse.find("select", {"class": "form-control input-lg selectpicker"})["data-balance-eur"] - - next_order_id, all_sales = get_sales(account) - - while next_order_id != None: - time.sleep(1) - next_order_id, new_sales = get_sales(account, start_from=next_order_id) - all_sales += new_sales + # Пытаемся получить актуальный выводимый баланс и валюту из страницы лота. + # Если верстка поменялась — используем fallback из объекта баланса. + try: + randomLotPageLink = bs(account.method("get", "https://funpay.com/lots/693/", {}, {}).text, "html.parser").find("a", {"class": "tc-item"})["href"] + randomLotPageParse = bs(account.method("get", randomLotPageLink, {}, {}).text, "html.parser") + + parsed_balance_text = randomLotPageParse.select_one(".badge-balance").text.split(" ") + balance_text_value, balance_text_currency = parsed_balance_text[0], parsed_balance_text[1] + + # Обновляем валюту и отображаемый баланс из страницы, если получилось распарсить + currency = balance_text_currency + balance_display = balance_text_value + + selectpicker = randomLotPageParse.find("select", {"class": "form-control input-lg selectpicker"}) + if currency == "₽": + canWithdraw["now"] = str(selectpicker.get("data-balance-rub", 0) or 0) + elif currency == "$": + canWithdraw["now"] = str(selectpicker.get("data-balance-usd", 0) or 0) + elif currency == "€": + canWithdraw["now"] = str(selectpicker.get("data-balance-eur", 0) or 0) + else: + canWithdraw["now"] = "0" + except Exception: + # Fallback: используем доступный баланс по выбранной валюте из объекта баланса + if currency == "₽": + canWithdraw["now"] = str(balance.available_rub) + elif currency == "$": + canWithdraw["now"] = str(balance.available_usd) + elif currency == "€": + canWithdraw["now"] = str(balance.available_eur) + balance_display = f"{balance_value}" + + # Пагинация продаж может быть длительной/нестабильной — обрабатываем сбои и ограничиваемся доступным + try: + next_order_id, all_sales = get_sales(account) + while next_order_id is not None: + time.sleep(1) + next_order_id, new_sales = get_sales(account, start_from=next_order_id) + all_sales += new_sales + except Exception: + all_sales = [] for sale in all_sales: if sale.status == OrderStatuses.REFUNDED: @@ -427,7 +453,7 @@ def generate_adv_profile(vertex: Vertex) -> str: return f"""Статистика аккаунта {account.username} ID: {account.id} -Баланс: {balance} {currency} +Баланс: {balance_display} {currency} Незавершенных заказов: {account.active_sales} Доступно для вывода From a0be5575dd3f2c249192d120e8550ecdb04991e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D1=96=D0=BD=D0=B0=20=D0=9A=D0=BE=D0=B2=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0?= Date: Fri, 5 Sep 2025 02:16:25 +0300 Subject: [PATCH 2/5] fix(bot): ACK in open_order_menu; chore(docker): linux paths; feat(utils): robust extract_float; feat(vertex): safe get_balance fallback --- Dockerfile | 7 +++++-- tg_bot/bot.py | 2 ++ tg_bot/utils.py | 30 ++++++++++++++++++++++++++---- vertex.py | 45 ++++++++++++++++++++++++++++++++++++--------- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 166d935..19f556b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ FROM python:3.10-slim -COPY . C:\app -WORKDIR C:\app +# Корректные Linux-пути для slim-образа +WORKDIR /app +COPY . /app + +ENV PYTHONUNBUFFERED=1 RUN python -m pip install --upgrade pip && \ python setup.py diff --git a/tg_bot/bot.py b/tg_bot/bot.py index b43b67a..1272b9c 100644 --- a/tg_bot/bot.py +++ b/tg_bot/bot.py @@ -790,6 +790,8 @@ def open_order_menu(self, c: CallbackQuery): node_id, username, order_id, no_refund = int(split[1]), split[2], split[3], bool(int(split[4])) self.bot.edit_message_reply_markup(c.message.chat.id, c.message.id, reply_markup=kb.new_order(order_id, username, node_id, no_refund=no_refund)) + # Подтверждаем callback, чтобы не показывался тайм-аут в клиенте Telegram + self.bot.answer_callback_query(c.id) # Панель управления def open_cp(self, c: CallbackQuery): diff --git a/tg_bot/utils.py b/tg_bot/utils.py index f417a54..18fb795 100644 --- a/tg_bot/utils.py +++ b/tg_bot/utils.py @@ -253,10 +253,32 @@ def message_hook(vertex: Vertex, event: NewMessageEvent): with open("storage/cache/advProfileStat.json", "w", encoding="UTF-8") as f: f.write(json.dumps(ORDER_CONFIRMED, indent=4, ensure_ascii=False)) -def extract_float(text): - cleaned_text = re.sub(r'[^\d.,]', '', text) - cleaned_text = cleaned_text.replace(',', '') - return float(cleaned_text) +def extract_float(text: str) -> float: + """ + Преобразует строку с ценой в число с плавающей точкой. + Поддерживает форматы с пробелами/неразрывными пробелами и запятой/точкой как разделителем. + """ + # Удаляем все, кроме цифр и разделителей, а также пробелы/неразрывные пробелы + s = re.sub(r"[^\d,\.]", "", text).replace("\u00A0", "").replace(" ", "") + if not s: + return 0.0 + # Если присутствуют и точка, и запятая — считаем правый разделитель десятичным, остальные убираем + if "," in s and "." in s: + if s.rfind(",") > s.rfind("."): + # десятичный разделитель — запятая + s = s.replace(".", "") + s = s.replace(",", ".") + else: + # десятичный разделитель — точка + s = s.replace(",", "") + else: + # Только запятая — заменяем на точку + if "," in s: + s = s.replace(",", ".") + try: + return float(s) + except ValueError: + return 0.0 def get_sales(account: Account, start_from: str | None = None, include_paid: bool = True, include_closed: bool = True, include_refunded: bool = True, exclude_ids: list[str] | None = None, diff --git a/vertex.py b/vertex.py index 34329ad..717d4c8 100644 --- a/vertex.py +++ b/vertex.py @@ -274,17 +274,44 @@ def __init_telegram(self) -> None: self.telegram.init() def get_balance(self, attempts: int = 3) -> FunPayAPI.types.Balance: + """ + Возвращает баланс аккаунта. Безопасно обрабатывает отсутствие лотов. + + Алгоритм: + - Пытаемся получить публичные лоты из случайной подкатегории несколько раз. + - Если лотов нет вовсе, пробуем запросить баланс на offer?id=0. + - Если и это не удалось — возвращаем нулевой баланс (чтобы не падать в /profile). + """ subcategories = self.account.get_sorted_subcategories()[FunPayAPI.enums.SubCategoryTypes.COMMON] - lots = [] - while not lots and attempts: + lots: list[FunPayAPI.types.LotShortcut] = [] + tried_subcats = set() + while attempts and len(tried_subcats) < len(subcategories): attempts -= 1 - subcat_id = random.choice(list(subcategories.keys())) - lots = self.account.get_subcategory_public_lots(FunPayAPI.enums.SubCategoryTypes.COMMON, subcat_id) - break - else: - raise Exception(...) - balance = self.account.get_balance(random.choice(lots).id) - return balance + try: + subcat_id = random.choice([i for i in subcategories.keys() if i not in tried_subcats]) + except IndexError: + break + tried_subcats.add(subcat_id) + try: + lots = self.account.get_subcategory_public_lots(FunPayAPI.enums.SubCategoryTypes.COMMON, subcat_id) + if lots: + break + except Exception: + continue + + # Если нашли лоты — используем любой для получения баланса + if lots: + try: + return self.account.get_balance(random.choice(lots).id) + except Exception: + pass + + # Фоллбек: пробуем offer?id=0 (часто достаточно для получения баланса) + try: + return self.account.get_balance(0) + except Exception: + # Возвращаем нулевой баланс как безопасный дефолт + return FunPayAPI.types.Balance(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) # Прочее def raise_lots(self) -> int: From 0e158fb01d3b9a62213a935d99e8c6c940d18b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D1=96=D0=BD=D0=B0=20=D0=9A=D0=BE=D0=B2=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0?= Date: Fri, 5 Sep 2025 12:34:25 +0300 Subject: [PATCH 3/5] tg_bot: fix adv profile stats by importing FunPayAPI.types as 'types' in utils; resolve silent failure in get_sales() --- tg_bot/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tg_bot/utils.py b/tg_bot/utils.py index 18fb795..1716ca2 100644 --- a/tg_bot/utils.py +++ b/tg_bot/utils.py @@ -21,6 +21,7 @@ from bs4 import BeautifulSoup from FunPayAPI.account import Account from FunPayAPI.types import OrderStatuses +import FunPayAPI.types as types from FunPayAPI.updater.events import * localizer = Localizer() _ = localizer.translate From 5fd93116ffa47555a9bacebafbf34ba669a6a812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D1=96=D0=BD=D0=B0=20=D0=9A=D0=BE=D0=B2=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0?= Date: Fri, 5 Sep 2025 12:42:41 +0300 Subject: [PATCH 4/5] tg_bot: adv stats now use Account.get_sells() + datetime windows (24h/7d/30d); fix crash from old get_sales signature --- tg_bot/utils.py | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/tg_bot/utils.py b/tg_bot/utils.py index 1716ca2..d42047a 100644 --- a/tg_bot/utils.py +++ b/tg_bot/utils.py @@ -416,55 +416,45 @@ def generate_adv_profile(vertex: Vertex) -> str: canWithdraw["now"] = str(balance.available_eur) balance_display = f"{balance_value}" - # Пагинация продаж может быть длительной/нестабильной — обрабатываем сбои и ограничиваемся доступным + # Получаем продажи через встроенный метод API с пагинацией try: - next_order_id, all_sales = get_sales(account) + next_order_id, all_sales = account.get_sells() while next_order_id is not None: time.sleep(1) - next_order_id, new_sales = get_sales(account, start_from=next_order_id) + next_order_id, new_sales = account.get_sells(start_from=next_order_id) all_sales += new_sales except Exception: all_sales = [] + now_dt = datetime.datetime.now() for sale in all_sales: - if sale.status == OrderStatuses.REFUNDED: + is_refund = sale.status == OrderStatuses.REFUNDED + delta = now_dt - sale.date if hasattr(sale, "date") else datetime.timedelta.max + + if is_refund: refunds["all"] += 1 refundsPrice["all"] += sale.price else: sales["all"] += 1 salesPrice["all"] += sale.price - upperDate = bs(sale.html, "html.parser").find("div", {"class": "tc-date-time"}).text - date = bs(sale.html, "html.parser").find("div", {"class": "tc-date-left"}).text - - if "сегодня" in upperDate or "сьогодні" in upperDate or "today" in upperDate: - if sale.status == OrderStatuses.REFUNDED: + # За сутки / неделю / месяц считаем по относительному времени + if delta <= datetime.timedelta(days=1): + if is_refund: refunds["day"] += 1 - refunds["week"] += 1 - refunds["month"] += 1 refundsPrice["day"] += sale.price - refundsPrice["week"] += sale.price - refundsPrice["month"] += sale.price else: sales["day"] += 1 - sales["week"] += 1 - sales["month"] += 1 salesPrice["day"] += sale.price - salesPrice["week"] += sale.price - salesPrice["month"] += sale.price - elif "день" in date or "дня" in date or "дней" in date or "дні" in date or "day" in date or "час" in date or "hour" in date or "годин" in date: - if sale.status == OrderStatuses.REFUNDED: + if delta <= datetime.timedelta(days=7): + if is_refund: refunds["week"] += 1 - refunds["month"] += 1 refundsPrice["week"] += sale.price - refundsPrice["month"] += sale.price else: sales["week"] += 1 - sales["month"] += 1 salesPrice["week"] += sale.price - salesPrice["month"] += sale.price - elif "недел" in date or "тижд" in date or "week" in date: - if sale.status == OrderStatuses.REFUNDED: + if delta <= datetime.timedelta(days=30): + if is_refund: refunds["month"] += 1 refundsPrice["month"] += sale.price else: From dfe7ec5e65bb1886cf304971cca1ee0b657df406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D1=96=D0=BD=D0=B0=20=D0=9A=D0=BE=D0=B2=D0=BF?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0?= Date: Fri, 5 Sep 2025 12:55:37 +0300 Subject: [PATCH 5/5] tg_bot: split adv stats by currency (USD/RUB/EUR); avoid mixing sums across currencies; keep withdraw predictions in account currency --- tg_bot/utils.py | 98 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/tg_bot/utils.py b/tg_bot/utils.py index d42047a..533df27 100644 --- a/tg_bot/utils.py +++ b/tg_bot/utils.py @@ -426,42 +426,88 @@ def generate_adv_profile(vertex: Vertex) -> str: except Exception: all_sales = [] + # Инициализируем агрегаты по валютам + currencies = ["USD", "RUB", "EUR"] + sym = {"USD": "$", "RUB": "₽", "EUR": "€"} + stats = { + cur: { + "sales": {"day": 0, "week": 0, "month": 0, "all": 0}, + "salesPrice": {"day": 0.0, "week": 0.0, "month": 0.0, "all": 0.0}, + "refunds": {"day": 0, "week": 0, "month": 0, "all": 0}, + "refundsPrice": {"day": 0.0, "week": 0.0, "month": 0.0, "all": 0.0}, + } + for cur in currencies + } + now_dt = datetime.datetime.now() for sale in all_sales: + cur = getattr(sale, "currency", None) + if cur not in stats: + # неизвестная валюта — пропускаем + continue is_refund = sale.status == OrderStatuses.REFUNDED delta = now_dt - sale.date if hasattr(sale, "date") else datetime.timedelta.max if is_refund: - refunds["all"] += 1 - refundsPrice["all"] += sale.price + stats[cur]["refunds"]["all"] += 1 + stats[cur]["refundsPrice"]["all"] += sale.price else: - sales["all"] += 1 - salesPrice["all"] += sale.price + stats[cur]["sales"]["all"] += 1 + stats[cur]["salesPrice"]["all"] += sale.price # За сутки / неделю / месяц считаем по относительному времени if delta <= datetime.timedelta(days=1): if is_refund: - refunds["day"] += 1 - refundsPrice["day"] += sale.price + stats[cur]["refunds"]["day"] += 1 + stats[cur]["refundsPrice"]["day"] += sale.price else: - sales["day"] += 1 - salesPrice["day"] += sale.price + stats[cur]["sales"]["day"] += 1 + stats[cur]["salesPrice"]["day"] += sale.price if delta <= datetime.timedelta(days=7): if is_refund: - refunds["week"] += 1 - refundsPrice["week"] += sale.price + stats[cur]["refunds"]["week"] += 1 + stats[cur]["refundsPrice"]["week"] += sale.price else: - sales["week"] += 1 - salesPrice["week"] += sale.price + stats[cur]["sales"]["week"] += 1 + stats[cur]["salesPrice"]["week"] += sale.price if delta <= datetime.timedelta(days=30): if is_refund: - refunds["month"] += 1 - refundsPrice["month"] += sale.price + stats[cur]["refunds"]["month"] += 1 + stats[cur]["refundsPrice"]["month"] += sale.price else: - sales["month"] += 1 - salesPrice["month"] += sale.price - - + stats[cur]["sales"]["month"] += 1 + stats[cur]["salesPrice"]["month"] += sale.price + + + + # Формируем блоки статистики по валютам + sales_blocks = [] + refunds_blocks = [] + for cur in currencies: + # Показываем только если есть данные по всем времени + if stats[cur]["sales"]["all"] or stats[cur]["refunds"]["all"]: + s = stats[cur]["sales"] + sp = stats[cur]["salesPrice"] + r = stats[cur]["refunds"] + rp = stats[cur]["refundsPrice"] + curr_sym = sym[cur] + sales_blocks.append( + f"{curr_sym}\n" + f"За день: {s['day']} ({sp['day']:.1f} {curr_sym})\n" + f"За неделю: {s['week']} ({sp['week']:.1f} {curr_sym})\n" + f"За месяц: {s['month']} ({sp['month']:.1f} {curr_sym})\n" + f"За всё время: {s['all']} ({sp['all']:.1f} {curr_sym})" + ) + refunds_blocks.append( + f"{curr_sym}\n" + f"За день: {r['day']} ({rp['day']:.1f} {curr_sym})\n" + f"За неделю: {r['week']} ({rp['week']:.1f} {curr_sym})\n" + f"За месяц: {r['month']} ({rp['month']:.1f} {curr_sym})\n" + f"За всё время: {r['all']} ({rp['all']:.1f} {curr_sym})" + ) + + sales_text = "\n\n".join(sales_blocks) if sales_blocks else "Нет данных" + refunds_text = "\n\n".join(refunds_blocks) if refunds_blocks else "Нет данных" return f"""Статистика аккаунта {account.username} @@ -475,17 +521,11 @@ def generate_adv_profile(vertex: Vertex) -> str: Через день: +{"{:.1f}".format(canWithdraw["day"])} {currency} Через 2 дня: +{"{:.1f}".format(canWithdraw["2day"])} {currency} -Товаров продано -За день: {sales["day"]} ({"{:.1f}".format(salesPrice["day"])} {currency}) -За неделю: {sales["week"]} ({"{:.1f}".format(salesPrice["week"])} {currency}) -За месяц: {sales["month"]} ({"{:.1f}".format(salesPrice["month"])} {currency}) -За всё время: {sales["all"]} ({"{:.1f}".format(salesPrice["all"])} {currency}) - -Товаров возвращено -За день: {refunds["day"]} ({"{:.1f}".format(refundsPrice["day"])} {currency}) -За неделю: {refunds["week"]} ({"{:.1f}".format(refundsPrice["week"])} {currency}) -За месяц: {refunds["month"]} ({"{:.1f}".format(refundsPrice["month"])} {currency}) -За всё время: {refunds["all"]} ({"{:.1f}".format(refundsPrice["all"])} {currency}) +Товаров продано (по валютам) +{sales_text} + +Товаров возвращено (по валютам) +{refunds_text} Обновлено: {time.strftime('%H:%M:%S', time.localtime(account.last_update))}"""