first commit

This commit is contained in:
2026-04-12 21:58:52 +03:00
commit acfaa2a40c
44 changed files with 2895 additions and 0 deletions

0
bot/utils/__init__.py Normal file
View File

View File

@@ -0,0 +1,34 @@
from decimal import Decimal, InvalidOperation
import re
def parse_rub_amount(raw_value: str) -> Decimal | None:
normalized_value = raw_value.lower().strip().replace(",", ".")
normalized_value = normalized_value.replace(" ", "")
normalized_value = re.sub(r"руб(лей|ля|ль|ле|\.?)", "", normalized_value)
normalized_value = re.sub(r"[^\d.]", "", normalized_value)
if not normalized_value or normalized_value.count(".") > 1:
return None
try:
amount = Decimal(normalized_value)
except InvalidOperation:
return None
if amount <= 0:
return None
if amount.as_tuple().exponent < -2:
return None
return amount.quantize(Decimal("0.01"))
def format_rub_amount(amount: Decimal) -> str:
normalized_amount = amount.quantize(Decimal("0.01"))
if normalized_amount == normalized_amount.to_integral():
return f"{int(normalized_amount)}"
return f"{normalized_amount:.2f}".replace(".", ",")

17
bot/utils/cfg_loader.py Normal file
View File

@@ -0,0 +1,17 @@
import simplejson as json
# init
CFG_PATH = "cfg/config.json"
# load cfg and return it
def load_config(cfg_path=CFG_PATH):
with open(cfg_path, "r", encoding="utf-8") as config_fp:
return json.load(config_fp)
def rewrite_config(obj, cfg_path=CFG_PATH):
with open(cfg_path, "w", encoding="utf-8") as config_fp:
json.dump(obj, config_fp, indent=4)

103
bot/utils/logging_config.py Normal file
View File

@@ -0,0 +1,103 @@
import json
import logging
import os
import sys
from datetime import datetime, timezone
STANDARD_LOG_RECORD_FIELDS = {
"args",
"asctime",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"message",
"msg",
"name",
"pathname",
"process",
"processName",
"relativeCreated",
"stack_info",
"thread",
"threadName",
"taskName",
}
class JsonFormatter(logging.Formatter):
def __init__(self, service_name: str):
super().__init__()
self.service_name = service_name
def format(self, record: logging.LogRecord) -> str:
log_payload = {
"timestamp": datetime.fromtimestamp(
record.created, tz=timezone.utc
).isoformat(),
"level": record.levelname,
"service": self.service_name,
"logger": record.name,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
extra_fields = self._collect_extra_fields(record)
if extra_fields:
log_payload["extra"] = extra_fields
if record.exc_info:
log_payload["exception"] = self.formatException(record.exc_info)
return json.dumps(log_payload, ensure_ascii=False)
def _collect_extra_fields(self, record: logging.LogRecord) -> dict:
extra_fields = {}
for key, value in record.__dict__.items():
if key in STANDARD_LOG_RECORD_FIELDS or key.startswith("_"):
continue
if isinstance(value, (str, int, float, bool)) or value is None:
extra_fields[key] = value
else:
extra_fields[key] = repr(value)
return extra_fields
def setup_logging(service_name: str) -> None:
log_level_name = os.getenv("LOG_LEVEL", "INFO").upper()
log_level = getattr(logging, log_level_name, logging.INFO)
root_logger = logging.getLogger()
root_logger.handlers.clear()
root_logger.setLevel(log_level)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter(service_name=service_name))
root_logger.addHandler(handler)
for logger_name in (
"uvicorn",
"uvicorn.error",
"uvicorn.access",
"aiogram",
"aiohttp",
"asyncio",
):
logger = logging.getLogger(logger_name)
logger.handlers.clear()
logger.propagate = True
logging.captureWarnings(True)

44
bot/utils/text_tools.py Normal file
View File

@@ -0,0 +1,44 @@
import re
def to_html(obj):
return str(obj).replace("<", "&lt;").replace(">", "&gt;")
def parse_links_to_inline_markup(message: str) -> list:
"""
Парсит сообщение с форматированными ссылками и возвращает список рядов кнопок.
Формат входного сообщения:
- [Текст кнопки + Ссылка] для одной кнопки.
- [Кнопка1 + Ссылка1][Кнопка2 + Ссылка2] для нескольких кнопок в одном ряду.
- Каждая строка представляет отдельный ряд кнопок.
Пример:
[Кнопка1 + https://example.com]
[Кнопка2 + https://example.org][Кнопка3 + https://example.net]
:param message: Строка с отформатированными ссылками.
:return: Список рядов кнопок, где каждый ряд — это список кортежей (Текст, Ссылка).
"""
# Исправленное регулярное выражение для поиска [Текст + Ссылка]
pattern = re.compile(r"\[([^\[\]+]+)\s*\+\s*(https?://[^\[\]]+)\]")
# Инициализируем список рядов кнопок
keyboard_rows = []
# Разбиваем сообщение на строки
lines = message.strip().split("\n")
for line in lines:
# Находим все совпадения в строке
matches = pattern.findall(line)
if matches:
row = []
for text, url in matches:
button = (text.strip(), url.strip())
row.append(button)
keyboard_rows.append(row)
return keyboard_rows