Files
Solo_bot/audit/rules.py
T

428 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from typing import Iterable
def _get_bot_webhook_path() -> str:
"""Путь вебхука бота из конфига (для исключения из шага «успешная оплата»)."""
try:
from config import WEBHOOK_PATH
return ((WEBHOOK_PATH or "").strip().lower()) or ""
except ImportError:
return ""
def _is_bot_webhook_path(path: str) -> bool:
"""True только если path — именно вебхук бота (точное совпадение сегмента пути), не касса."""
bot_path = _get_bot_webhook_path()
if not bot_path:
return False
p = (path or "").strip().lower()
path_segment = p.split(" ", 1)[1] if " " in p else p
return path_segment == bot_path or path_segment.rstrip("/") == bot_path.rstrip("/")
AUDIT_STEP_LABELS: dict[str, str] = {
"start": "Старт",
"start_coupon": "Старт: купон",
"start_gift": "Старт: подарок",
"start_referral": "Старт: рефералка",
"start_utm": "Старт: UTM",
"profile": "Профиль",
"about": "О VPN",
"instructions": "Инструкции",
"balance": "Баланс / история оплат",
"view_keys": "Мои ключи",
"buy_entry": "Оформление: вход",
"tariff_config": "Оформление: выбор тарифа/конфига",
"key_create": "Подписка оформлена (ключ создан)",
"pay_start": "Оплата: вход / создание ссылки",
"pay": "Успешная оплата (пополнение)",
"key_view": "Ключ (карточка)",
"connect": "Подключение: экран / инструкции / QR",
"key_manage": "Управление подпиской",
"renew": "Продление",
"addons": "Аддоны",
"referral": "Рефералы",
"coupons": "Купоны",
"register": "Регистрация (API)",
"login": "Вход (API)",
"api_other": "API прочее",
"admin": "Админ-панель",
"other": "Прочее",
}
DEFAULT_FUNNEL_STEPS = (
"start",
"profile",
"view_keys",
"buy_entry",
"tariff_config",
"key_create",
"pay_start",
"pay",
"key_view",
"connect",
)
_CALLBACK_EXACT: dict[str, str] = {
"start": "start",
"profile": "profile",
"about_vpn": "about",
"instructions": "instructions",
"view_keys": "view_keys",
"create_key": "buy_entry",
"buy": "buy_entry",
"pay": "pay_start",
"balance": "balance",
"balance_history": "balance",
"activate_coupon": "coupons",
"cancel_coupon_activation": "coupons",
"exit_coupon_input": "coupons",
"invite": "referral",
"top_referrals": "referral",
"check_subscription": "start",
"back_to_tariff_group_list": "tariff_config",
"back_to_subgroup_tariffs": "tariff_config",
"cancel_and_back_to_view_keys": "view_keys",
"fastflow_coupon": "coupons",
"fastflow_coupon_back": "coupons",
"fastflow_back": "pay_start",
"pay_kassai": "pay_start",
"pay_kassai_cards": "pay_start",
"pay_kassai_sbp": "pay_start",
"pay_heleket_crypto": "pay_start",
"pay_freekassa": "pay_start",
"pay_robokassa": "pay_start",
}
_CALLBACK_PREFIX: list[tuple[str, str]] = [
("view_key|", "key_view"),
("view_keys|", "view_keys"),
("show_referral_qr|", "referral"),
("tariff_subgroup_user|", "tariff_config"),
("select_tariff_plan|", "tariff_config"),
("cfg_user_devices|", "tariff_config"),
("cfg_user_traffic|", "tariff_config"),
("rename_key|", "key_manage"),
("reset_hwid|", "key_manage"),
("delete_key|", "key_manage"),
("confirm_delete|", "key_manage"),
("update_subscription|", "key_manage"),
("change_location|", "key_manage"),
("select_country|", "key_manage"),
("connect_device|", "connect"),
("connect_router|", "connect"),
("connect_tv|", "connect"),
("connect_pc|", "connect"),
("connect_ios|", "connect"),
("connect_android|", "connect"),
("show_qr|", "connect"),
("continue_tv|", "connect"),
("pay_currency|", "pay_start"),
("choose_payment_currency|", "pay_start"),
("cfg_user_confirm|", "key_create"),
("choose_payment_provider|", "pay_start"),
("kassai_method|", "pay_start"),
("kassai_cards_amount|", "pay_start"),
("kassai_sbp_amount|", "pay_start"),
("kassai_custom_amount|", "pay_start"),
("heleket_method|", "pay_start"),
("heleket_crypto_amount|", "pay_start"),
("heleket_custom_amount|", "pay_start"),
("freekassa_amount|", "pay_start"),
("robokassa_", "pay_start"),
("cfg_renew", "renew"),
("key_addons", "addons"),
("extend_key", "coupons"),
]
_CALLBACK_CONTAINS: list[tuple[str, str]] = [
("connect_", "connect"),
("renew", "renew"),
("addon", "addons"),
("referral", "referral"),
("invite", "referral"),
("coupon", "coupons"),
("users_audit", "admin"),
("users_editor", "admin"),
("search_user", "admin"),
("admin_panel", "admin"),
]
_MESSAGE_STEP_BY_COMMAND: dict[str, str] = {
"/start": "start",
"start": "start",
"/buy": "buy_entry",
"buy": "buy_entry",
"/subs": "view_keys",
"subs": "view_keys",
"/profile": "profile",
"profile": "profile",
"/invite": "referral",
"invite": "referral",
"/instructions": "instructions",
"instructions": "instructions",
"/activate_coupon": "coupons",
"activate_coupon": "coupons",
}
_START_PAYLOAD_MAX_LEN = 256
_START_PAYLOAD_MAX_PARTS = 20
_IGNORED_CALLBACK_EXACT: set[str] = {
" ",
"back",
"cancel",
"back_to_pay",
"back_to_currency",
"back_to_tariff_group_list",
"back_to_subgroup_tariffs",
"cancel_and_back_to_view_keys",
"cancel_coupon_activation",
"exit_coupon_input",
"fastflow_back",
"fastflow_coupon_back",
"cfg_back_menu",
"cfg_cancel_input",
"cancel_broadcast",
}
_IGNORED_CALLBACK_PREFIX: tuple[str, ...] = (
"back:",
"back_to_",
"cancel_",
"gifts_page|",
)
_IGNORED_CALLBACK_EXACT_RULES: dict[str, str] = {key: "ignore" for key in _IGNORED_CALLBACK_EXACT}
_IGNORED_CALLBACK_PREFIX_RULES: tuple[tuple[str, str], ...] = tuple(
(prefix, "ignore") for prefix in _IGNORED_CALLBACK_PREFIX
)
_HANDLER_CONTAINS: list[tuple[str, str] | tuple[str, str, str]] = [
("process_start", "start"),
("start_entry", "start"),
("show_start_menu", "start"),
("process_callback_view_profile", "profile"),
("handle_about_vpn", "about"),
("send_instructions", "instructions"),
("process_callback_or_message_view_keys", "view_keys"),
("key_view", "key_view", "key_create"),
("handle_user_config_confirm", "key_create"),
("finalize_config_and_purchase", "key_create"),
("proceed_purchase_with_values", "tariff_config"),
("key_create", "buy_entry"),
("handle_key_creation", "buy_entry"),
("complete_key_renewal", "renew"),
("handle_connect_device", "connect"),
("process_connect_", "connect"),
("process_callback_connect", "connect"),
("process_continue_tv", "connect"),
("show_qr_code", "connect"),
("balance_history", "balance"),
("balance_handler", "balance"),
("pay", "pay_start"),
("choose_payment_provider", "pay_start"),
("kassai_", "pay_start"),
("heleket_", "pay_start"),
("freekassa_", "pay_start"),
("robokassa_", "pay_start"),
("rename_key", "key_manage"),
("reset_hwid", "key_manage"),
("delete_key", "key_manage"),
("change_location", "key_manage"),
("select_country", "key_manage"),
("renew", "renew"),
("addon", "addons"),
("referral", "referral"),
("refferal", "referral"),
("coupon", "coupons"),
("admin_panel", "admin"),
("users_audit", "admin"),
("users_editor", "admin"),
("search_user", "admin"),
("auth/register", "register"),
("auth/login", "login"),
("auth/send-login", "login"),
("auth/login-by-code", "login"),
("auth/login-telegram", "login"),
]
_API_CONTAINS: list[tuple[str, str]] = [
("auth/register", "register"),
("auth/login", "login"),
("auth/send-login", "login"),
("auth/login-by-code", "login"),
("auth/login-telegram", "login"),
("/keys/create", "key_create"),
]
def _match_step_rules(
value: str,
*,
exact: dict[str, str] | None = None,
prefixes: Iterable[tuple[str, str]] | None = None,
contains: Iterable[tuple[str, str] | tuple[str, str, str]] | None = None,
) -> str | None:
"""Универсальный matcher шага по exact/prefix/contains правилам."""
normalized = (value or "").lower().strip()
if not normalized:
return None
if exact:
step = exact.get(normalized)
if step:
return step
if prefixes:
for prefix, step in prefixes:
if normalized.startswith(prefix):
return step
if contains:
for item in contains:
substr, step = item[0], item[1]
exclude = item[2] if len(item) > 2 else None
if substr in normalized and (exclude is None or exclude not in normalized):
return step
return None
def _is_ignored_analytics_event(path: str) -> bool:
"""Исключает чисто навигационные события из аналитики шагов и воронки."""
p = (path or "").lower().strip()
if not p.startswith("callback:"):
return False
callback_data = p.split(":", 1)[-1]
return _match_step_rules(
callback_data,
exact=_IGNORED_CALLBACK_EXACT_RULES,
prefixes=_IGNORED_CALLBACK_PREFIX_RULES,
) is not None
def _message_command_step(path: str) -> str | None:
"""Определяет шаг только по точной команде/первому токену сообщения."""
steps = _message_command_steps(path)
return steps[0] if steps else None
def _message_command_steps(path: str) -> list[str]:
"""Определяет один или несколько шагов из текстового сообщения."""
p = (path or "").strip()
if not p.lower().startswith("message:"):
return []
text = (p.split(":", 1)[-1] or "").strip().lower()
if not text:
return []
parts = text.split(None, 1)
token = parts[0]
if token.startswith("/"):
token = token.split("@", 1)[0]
if token in ("/start", "start"):
steps = ["start"]
payload = parts[1].strip() if len(parts) > 1 else ""
if payload:
if len(payload) > _START_PAYLOAD_MAX_LEN:
payload = payload[:_START_PAYLOAD_MAX_LEN]
payload_parts = payload.split("-")
if len(payload_parts) > _START_PAYLOAD_MAX_PARTS:
payload_parts = payload_parts[:_START_PAYLOAD_MAX_PARTS]
for part in payload_parts:
part = part.strip()
if not part:
continue
if "coupons" in part:
steps.append("start_coupon")
continue
if "gift" in part:
steps.append("start_gift")
continue
if "referral" in part:
steps.append("start_referral")
continue
if "utm" in part:
steps.append("start_utm")
continue
normalized_payload = "-".join(payload_parts).strip().lower()
if normalized_payload in _MESSAGE_STEP_BY_COMMAND:
steps.append(_MESSAGE_STEP_BY_COMMAND[normalized_payload])
return list(dict.fromkeys(steps))
return steps
step = _MESSAGE_STEP_BY_COMMAND.get(token)
return [step] if step else []
def _callback_step(path: str) -> str | None:
callback_data = path.split(":", 1)[-1]
callback_key = callback_data.split("|")[0]
return _match_step_rules(
callback_key,
exact=_CALLBACK_EXACT,
) or _match_step_rules(
callback_data,
prefixes=_CALLBACK_PREFIX,
contains=_CALLBACK_CONTAINS,
)
def _api_step(path: str) -> str:
step = _match_step_rules(path, contains=_API_CONTAINS)
if step:
return step
if "payment-links" in path or ("payment" in path and "webhook" not in path):
return "pay_start"
return "api_other"
def _handler_step(path: str) -> str | None:
return _match_step_rules(path, contains=_HANDLER_CONTAINS)
def _funnel_step_counts(path: str, result: str, step: str) -> bool:
"""Решает, считать ли событие достижением шага воронки."""
if result != "success":
return False
p = (path or "").lower()
if step == "pay_start":
return True
if step == "pay":
return p.startswith("payment_success:")
if step == "key_create":
return "cfg_user_confirm" in p or "/keys/create" in p
return True
def _normalize_path_to_step(path: str) -> str:
"""Сводит path_or_handler к шагу по правилам из маппингов."""
steps = _normalize_path_to_steps(path)
return steps[0] if steps else "other"
def _normalize_path_to_steps(path: str) -> list[str]:
"""Сводит path_or_handler к одному или нескольким шагам по правилам аудита."""
if not path:
return ["other"]
p = path.lower().strip()
if p.startswith("payment_success:"):
return ["pay"]
if p.startswith("callback:"):
return [_callback_step(p) or "other"]
message_steps = _message_command_steps(path)
if message_steps:
return message_steps
if p.startswith("message:"):
return ["other"]
if p.startswith("post ") or p.startswith("get "):
return [_api_step(p)]
return [_handler_step(p) or "other"]