diff --git a/README.md b/README.md
index a1524764..1878f8c4 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,8 @@
| **Полный контроль над клиентом** | • Просмотр ключа, сервера и оставшегося времени через админку
• Продление и удаление ключей, начисление дней, отключение клиента
• Смена локации между серверами
• Поддержка нескольких устройств |
| **Реферальная программа** | • Уникальные ссылки для приглашений
• Инлайн-режим и обычные сообщения
• Награда: процент или фиксированная сумма за пополнение |
| **UTM-аналитика** | • Отслеживание рекламных переходов
• Привязка по рефералам, купонам, пробникам
• Анализ конверсий: регистрации, покупки, триалы
• Удалённый просмотр и контроль через админку |
-| **Поддержка платёжных систем** | • YooKassa (ИП / Самозанятые)
• YooMoney (Физ. лица) [@TrackLine](https://github.com/TrackLine)
• Robokassa (ИП)
• Cryptobot [@izzzzzi](https://github.com/izzzzzi)
• Telegram Stars |
+| **Поддержка платёжных систем** | • YooKassa (ИП / Самозанятые)
• YooMoney (Физ. лица) [@TrackLine](https://github.com/TrackLine)
• Robokassa (ИП)
• Cryptobot [@izzzzzi](https://github.com/izzzzzi)
• Telegram Stars
• Heleket (Физ. лица) [@JustYay](https://github.com/JustYay)
• Wata (Физ. лица) [@TrackLine](https://github.com/TrackLine)
• Kassai (Физ. лица) [@JustYay](https://github.com/JustYay)
+ |
| **Безопасность и стабильность** | • Периодические бэкапы
• Смена домена в случае переезда
• Проверка доступности серверов
• Уведомление о недоступности сервера и его аптайм
|
| **Уведомления** | • Напоминания об истекающих подписках (24ч / 6ч / момент)
• Напоминания о неиспользованном трафике |
| **Серверная часть** | • Мультисерверность (добавление серверов в неограниченном количестве)
• Выдача в разных режимах (по одной локации или в формате подписки)
• Автопроверка доступности
• Балансировка нагрузки при выдаче ключей
• Синхронизация клиентов между серверами
• Ограничение максимального количества ключей на сервер
• Возможность включения/отключения отдельных серверов |
diff --git a/api/routes/keys.py b/api/routes/keys.py
index da06675e..729cc242 100644
--- a/api/routes/keys.py
+++ b/api/routes/keys.py
@@ -75,7 +75,6 @@ async def edit_key_by_email(
session: AsyncSession = Depends(get_session),
admin: Admin = Depends(verify_admin_token),
):
-
result = await session.execute(select(Key).where(Key.email == email))
db_key = result.scalar_one_or_none()
if not db_key:
@@ -121,7 +120,6 @@ async def create_key_api(
session: AsyncSession = Depends(get_session),
admin: Admin = Depends(verify_admin_token),
):
-
try:
await create_key_on_cluster(
cluster_id=payload.cluster_id,
diff --git a/bot.py b/bot.py
index f7bd627f..26bce7fe 100644
--- a/bot.py
+++ b/bot.py
@@ -1,7 +1,8 @@
import os
import subprocess
-import traceback
import time
+import traceback
+
from functools import lru_cache
from aiogram import Bot, Dispatcher
@@ -40,23 +41,25 @@ def _get_git_commit_number_uncached() -> str:
env["GIT_WORK_TREE"] = cwd
try:
- local_number = subprocess.check_output(
- ["git", "rev-list", "--count", "HEAD"], cwd=cwd, env=env
- ).decode().strip()
- local_hash = subprocess.check_output(
- ["git", "rev-parse", "HEAD"], cwd=cwd, env=env
- ).decode().strip()
+ local_number = (
+ subprocess.check_output(["git", "rev-list", "--count", "HEAD"], cwd=cwd, env=env).decode().strip()
+ )
+ local_hash = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=cwd, env=env).decode().strip()
try:
- branch = subprocess.check_output(
- ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, env=env
- ).decode().strip()
+ branch = (
+ subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, env=env).decode().strip()
+ )
if branch == "HEAD":
- describe = subprocess.check_output(
- ["git", "describe", "--tags", "--exact-match"],
- cwd=cwd,
- env=env,
- stderr=subprocess.DEVNULL,
- ).decode().strip()
+ describe = (
+ subprocess.check_output(
+ ["git", "describe", "--tags", "--exact-match"],
+ cwd=cwd,
+ env=env,
+ stderr=subprocess.DEVNULL,
+ )
+ .decode()
+ .strip()
+ )
branch = "main" if describe.startswith("v") or "release" in describe.lower() else "dev"
except Exception:
branch = "dev"
@@ -72,9 +75,9 @@ def _get_git_commit_number_uncached() -> str:
).decode()
remote_hash = remote_commit.split()[0]
- remote_number = subprocess.check_output(
- ["git", "rev-list", "--count", remote_hash], cwd=cwd, env=env
- ).decode().strip()
+ remote_number = (
+ subprocess.check_output(["git", "rev-list", "--count", remote_hash], cwd=cwd, env=env).decode().strip()
+ )
if local_hash == remote_hash:
logger.info("[Git] Локальная версия актуальна")
diff --git a/cli_launcher.py b/cli_launcher.py
index 7c585ead..c82629f1 100755
--- a/cli_launcher.py
+++ b/cli_launcher.py
@@ -1,17 +1,17 @@
import os
import re
+import shutil
import subprocess
import sys
-import shutil
-from rich.progress import Progress, SpinnerColumn, TextColumn
+
from time import sleep
-from rich.live import Live
-from rich.panel import Panel
-from rich.console import Group
import requests
-from rich.console import Console
+from rich.console import Console, Group
+from rich.live import Live
+from rich.panel import Panel
+from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.prompt import Confirm, Prompt
from rich.table import Table
@@ -61,7 +61,6 @@ def print_logo():
"╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ",
]
-
with Live(refresh_per_second=10) as live:
display = []
for line in logo_lines:
@@ -220,7 +219,7 @@ def install_dependencies():
progress.update(task_id, description="Установка зависимостей...")
subprocess.run(
- f"bash -c 'source venv/bin/activate && pip install -r requirements.txt'",
+ "bash -c 'source venv/bin/activate && pip install -r requirements.txt'",
shell=True,
check=True,
)
diff --git a/database/db.py b/database/db.py
index 21d5aaf6..6305e9fc 100644
--- a/database/db.py
+++ b/database/db.py
@@ -4,14 +4,7 @@ from sqlalchemy.orm import declarative_base
from config import DATABASE_URL
-engine = create_async_engine(
- DATABASE_URL,
- echo=False,
- future=True,
- pool_size=20,
- max_overflow=30,
- pool_timeout=15
-)
+engine = create_async_engine(DATABASE_URL, echo=False, future=True, pool_size=20, max_overflow=30, pool_timeout=15)
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
diff --git a/database/importer.py b/database/importer.py
index 3dc8d5a0..396e224f 100644
--- a/database/importer.py
+++ b/database/importer.py
@@ -8,8 +8,8 @@ from itertools import cycle
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
-from config import USE_COUNTRY_SELECTION
+from config import USE_COUNTRY_SELECTION
from database.models import Key, Server, User
@@ -18,14 +18,11 @@ async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple
skipped = 0
if USE_COUNTRY_SELECTION:
- result = await session.execute(
- select(Server.name)
- .where(Server.enabled == True, Server.panel_type == "3x-ui")
- )
+ result = await session.execute(select(Server.name).where(Server.enabled is True, Server.panel_type == "3x-ui"))
else:
result = await session.execute(
select(Server.cluster_name)
- .where(Server.enabled == True, Server.panel_type == "3x-ui", Server.cluster_name.isnot(None))
+ .where(Server.enabled is True, Server.panel_type == "3x-ui", Server.cluster_name.isnot(None))
.distinct()
)
@@ -121,4 +118,4 @@ async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple
continue
await session.commit()
- return imported, skipped
\ No newline at end of file
+ return imported, skipped
diff --git a/handlers/admin/ads/ads_handler.py b/handlers/admin/ads/ads_handler.py
index dfabee49..e2d81294 100644
--- a/handlers/admin/ads/ads_handler.py
+++ b/handlers/admin/ads/ads_handler.py
@@ -1,4 +1,5 @@
import re
+
from datetime import datetime
import pytz
@@ -177,7 +178,7 @@ def format_ads_stats(stats: dict, username_bot: str) -> str:
moscow_tz = pytz.timezone("Europe/Moscow")
now = datetime.now(moscow_tz)
update_time = now.strftime("%d.%m.%y %H:%M:%S")
-
+
return (
f"📊 Статистика по рекламной ссылке\n\n"
f"📌 Название: {stats['name']}\n"
diff --git a/handlers/admin/bans/bans_handler.py b/handlers/admin/bans/bans_handler.py
index 215f96ff..a4c4d7c7 100644
--- a/handlers/admin/bans/bans_handler.py
+++ b/handlers/admin/bans/bans_handler.py
@@ -1,14 +1,15 @@
import csv
import io
+
from datetime import datetime, timezone
-from aiogram.fsm.context import FSMContext
-from sqlalchemy.dialects.postgresql import insert as pg_insert
from aiogram import F, Router
+from aiogram.fsm.context import FSMContext
+from aiogram.fsm.state import State, StatesGroup
from aiogram.types import BufferedInputFile, CallbackQuery, Message
from sqlalchemy import delete, text
+from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
-from aiogram.fsm.state import State, StatesGroup
from database import delete_user_data
from database.models import ManualBan
@@ -168,23 +169,27 @@ async def handle_preemptive_ids_input(message: Message, state: FSMContext, sessi
now = datetime.now(timezone.utc)
- stmt = pg_insert(ManualBan).values([
- {
- "tg_id": tg_id,
- "reason": "shadow",
- "banned_by": message.from_user.id,
- "until": None,
- "banned_at": now,
- }
- for tg_id in tg_ids
- ]).on_conflict_do_update(
- index_elements=[ManualBan.tg_id],
- set_={
- "reason": "shadow",
- "until": None,
- "banned_by": message.from_user.id,
- "banned_at": now,
- },
+ stmt = (
+ pg_insert(ManualBan)
+ .values([
+ {
+ "tg_id": tg_id,
+ "reason": "shadow",
+ "banned_by": message.from_user.id,
+ "until": None,
+ "banned_at": now,
+ }
+ for tg_id in tg_ids
+ ])
+ .on_conflict_do_update(
+ index_elements=[ManualBan.tg_id],
+ set_={
+ "reason": "shadow",
+ "until": None,
+ "banned_by": message.from_user.id,
+ "banned_at": now,
+ },
+ )
)
await session.execute(stmt)
diff --git a/handlers/admin/gifts/gifts_handler.py b/handlers/admin/gifts/gifts_handler.py
index f9bedbfd..9d31985e 100644
--- a/handlers/admin/gifts/gifts_handler.py
+++ b/handlers/admin/gifts/gifts_handler.py
@@ -83,7 +83,9 @@ async def admin_gift_show_tariffs_in_subgroup(callback: CallbackQuery, session:
return
stmt = (
- select(Tariff).where(Tariff.group_code == "gifts", Tariff.is_active.is_(True)).order_by(Tariff.duration_days)
+ select(Tariff)
+ .where(Tariff.group_code == "gifts", Tariff.is_active.is_(True))
+ .order_by(Tariff.duration_days)
)
result = await session.execute(stmt)
tariffs = result.scalars().all()
diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py
index 9e9ff16c..de53cdd4 100644
--- a/handlers/admin/management/management_handler.py
+++ b/handlers/admin/management/management_handler.py
@@ -661,7 +661,6 @@ async def prompt_for_file_upload(callback: CallbackQuery, state: FSMContext):
await state.set_state(FileUploadState.waiting_for_file)
-
@router.message(FileUploadState.waiting_for_file, F.document)
async def handle_admin_file_upload(message: Message, state: FSMContext):
document = message.document
@@ -687,4 +686,3 @@ async def handle_admin_file_upload(message: Message, state: FSMContext):
reply_markup=build_admin_back_kb("management"),
)
await state.clear()
-
diff --git a/handlers/admin/sender/sender_handler.py b/handlers/admin/sender/sender_handler.py
index 031051a3..1b10b338 100644
--- a/handlers/admin/sender/sender_handler.py
+++ b/handlers/admin/sender/sender_handler.py
@@ -20,38 +20,28 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from .keyboard import AdminSenderCallback, build_clusters_kb, build_sender_kb
-
router = Router()
async def send_broadcast_batch(bot, messages, batch_size=15):
results = []
-
+
for i in range(0, len(messages), batch_size):
- batch = messages[i:i + batch_size]
+ batch = messages[i : i + batch_size]
tasks = []
-
+
for msg in batch:
tg_id = msg["tg_id"]
text = msg["text"]
photo = msg.get("photo")
keyboard = msg.get("keyboard")
-
+
if photo:
task = bot.send_photo(
- chat_id=tg_id,
- photo=photo,
- caption=text,
- parse_mode="HTML",
- reply_markup=keyboard
+ chat_id=tg_id, photo=photo, caption=text, parse_mode="HTML", reply_markup=keyboard
)
else:
- task = bot.send_message(
- chat_id=tg_id,
- text=text,
- parse_mode="HTML",
- reply_markup=keyboard
- )
+ task = bot.send_message(chat_id=tg_id, text=text, parse_mode="HTML", reply_markup=keyboard)
tasks.append(task)
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
@@ -65,7 +55,7 @@ async def send_broadcast_batch(bot, messages, batch_size=15):
if i + batch_size < len(messages):
await asyncio.sleep(1.0)
-
+
return results
@@ -292,20 +282,11 @@ async def handle_send_confirm(callback_query: CallbackQuery, state: FSMContext,
messages = []
for tg_id in tg_ids:
- message_data = {
- "tg_id": tg_id,
- "text": text_message,
- "photo": photo,
- "keyboard": keyboard
- }
+ message_data = {"tg_id": tg_id, "text": text_message, "photo": photo, "keyboard": keyboard}
messages.append(message_data)
- results = await send_broadcast_batch(
- bot=callback_query.bot,
- messages=messages,
- batch_size=15
- )
-
+ results = await send_broadcast_batch(bot=callback_query.bot, messages=messages, batch_size=15)
+
success_count = sum(1 for result in results if result)
await callback_query.message.answer(
diff --git a/handlers/admin/stats/stats_handler.py b/handlers/admin/stats/stats_handler.py
index 2865a9d3..3e1ad2f6 100644
--- a/handlers/admin/stats/stats_handler.py
+++ b/handlers/admin/stats/stats_handler.py
@@ -150,10 +150,14 @@ async def handle_stats(callback_query: CallbackQuery, session: AsyncSession):
total_referrals = await count_total_referrals(session)
total_payments_today = await sum_payments_since(session, today_start.replace(tzinfo=None))
- total_payments_yesterday = await sum_payments_between(session, yesterday_start.replace(tzinfo=None), yesterday_end.replace(tzinfo=None))
+ total_payments_yesterday = await sum_payments_between(
+ session, yesterday_start.replace(tzinfo=None), yesterday_end.replace(tzinfo=None)
+ )
total_payments_week = await sum_payments_since(session, week_start.replace(tzinfo=None))
total_payments_month = await sum_payments_since(session, month_start.replace(tzinfo=None))
- total_payments_last_month = await sum_payments_between(session, last_month_start.replace(tzinfo=None), last_month_end.replace(tzinfo=None))
+ total_payments_last_month = await sum_payments_between(
+ session, last_month_start.replace(tzinfo=None), last_month_end.replace(tzinfo=None)
+ )
total_payments_all_time = await sum_total_payments(session)
hot_leads_count = await count_hot_leads(session)
diff --git a/handlers/admin/tariffs/tariffs_handler.py b/handlers/admin/tariffs/tariffs_handler.py
index 3e16e198..f4a939da 100644
--- a/handlers/admin/tariffs/tariffs_handler.py
+++ b/handlers/admin/tariffs/tariffs_handler.py
@@ -1,6 +1,7 @@
-from datetime import datetime
import re
+from datetime import datetime
+
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
@@ -15,7 +16,7 @@ from sqlalchemy import delete, distinct, or_, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from database import create_tariff
-from database.models import Key, Server, Tariff, Gift
+from database.models import Gift, Key, Server, Tariff
from database.tariffs import create_subgroup_hash, find_subgroup_by_hash
from filters.admin import IsAdminFilter
@@ -348,9 +349,7 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT
if group_code == "gifts":
gift_check = await session.execute(select(Gift).where(Gift.tariff_id == tariff_id).limit(1))
if gift_check.scalar_one_or_none():
- result = await session.execute(
- select(Tariff).where(Tariff.group_code == "gifts", Tariff.id != tariff_id)
- )
+ result = await session.execute(select(Tariff).where(Tariff.group_code == "gifts", Tariff.id != tariff_id))
other_tariffs = result.scalars().all()
if not other_tariffs:
@@ -372,16 +371,14 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT
for t in other_tariffs:
builder.button(
text=f"{t.name} — {t.price_rub}₽",
- callback_data=f"confirm_delete_tariff_with_replace|{tariff_id}|{t.id}"
+ callback_data=f"confirm_delete_tariff_with_replace|{tariff_id}|{t.id}",
)
- builder.button(
- text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
- )
+ builder.button(text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack())
await callback.message.edit_text(
"Этот тариф используется в подарках.\n\n"
"Выберите тариф, на который заменить его во всех подарках перед удалением:",
- reply_markup=builder.as_markup()
+ reply_markup=builder.as_markup(),
)
return
@@ -391,7 +388,9 @@ async def confirm_tariff_deletion(callback: CallbackQuery, callback_data: AdminT
inline_keyboard=[
[
InlineKeyboardButton(text="✅ Да", callback_data=f"confirm_delete_tariff|{tariff_id}"),
- InlineKeyboardButton(text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()),
+ InlineKeyboardButton(
+ text="❌ Отмена", callback_data=AdminTariffCallback(action=f"view|{tariff_id}").pack()
+ ),
]
]
),
@@ -404,9 +403,7 @@ async def delete_tariff_with_gift_replacement(callback: CallbackQuery, session:
tariff_id = int(tariff_id_str)
replacement_id = int(replacement_id_str)
- await session.execute(
- update(Gift).where(Gift.tariff_id == tariff_id).values(tariff_id=replacement_id)
- )
+ await session.execute(update(Gift).where(Gift.tariff_id == tariff_id).values(tariff_id=replacement_id))
await session.execute(update(Key).where(Key.tariff_id == tariff_id).values(tariff_id=None))
@@ -467,7 +464,6 @@ async def delete_tariff(callback: CallbackQuery, session: AsyncSession):
await callback.message.edit_text("🗑 Тариф успешно удалён.", reply_markup=build_tariff_menu_kb())
-
@router.callback_query(F.data.startswith("edit_field|"), IsAdminFilter())
async def ask_new_value(callback: CallbackQuery, state: FSMContext):
_, _tariff_id, field = callback.data.split("|")
diff --git a/handlers/admin/users/keyboard.py b/handlers/admin/users/keyboard.py
index 3f1f79e6..d1ceaf6c 100644
--- a/handlers/admin/users/keyboard.py
+++ b/handlers/admin/users/keyboard.py
@@ -350,34 +350,26 @@ def build_user_ban_type_kb(tg_id: int) -> InlineKeyboardMarkup:
builder.row(
InlineKeyboardButton(
text="⛔ Навсегда",
- callback_data=AdminUserEditorCallback(
- action="users_ban_forever", tg_id=tg_id
- ).pack(),
+ callback_data=AdminUserEditorCallback(action="users_ban_forever", tg_id=tg_id).pack(),
),
InlineKeyboardButton(
text="⏳ По сроку",
- callback_data=AdminUserEditorCallback(
- action="users_ban_temporary", tg_id=tg_id
- ).pack(),
+ callback_data=AdminUserEditorCallback(action="users_ban_temporary", tg_id=tg_id).pack(),
),
)
builder.row(
InlineKeyboardButton(
text="👻 Теневой бан",
- callback_data=AdminUserEditorCallback(
- action="users_ban_shadow", tg_id=tg_id
- ).pack(),
+ callback_data=AdminUserEditorCallback(action="users_ban_shadow", tg_id=tg_id).pack(),
)
)
builder.row(
InlineKeyboardButton(
text="⬅️ Назад",
- callback_data=AdminUserEditorCallback(
- action="users_editor", tg_id=tg_id, edit=True
- ).pack(),
+ callback_data=AdminUserEditorCallback(action="users_editor", tg_id=tg_id, edit=True).pack(),
)
)
- return builder.as_markup()
\ No newline at end of file
+ return builder.as_markup()
diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py
index 3b31ace6..14684a33 100644
--- a/handlers/admin/users/users_handler.py
+++ b/handlers/admin/users/users_handler.py
@@ -60,13 +60,13 @@ from .keyboard import (
build_hwid_menu_kb,
build_key_delete_kb,
build_key_edit_kb,
+ build_user_ban_type_kb,
build_user_delete_kb,
build_user_edit_kb,
build_users_balance_change_kb,
build_users_balance_kb,
build_users_key_expiry_kb,
build_users_key_show_kb,
- build_user_ban_type_kb
)
@@ -1629,10 +1629,7 @@ async def handle_ban_forever_reason_input(message: Message, state: FSMContext, s
await state.clear()
await message.answer(
- text=(
- f"✅ Пользователь {tg_id} забанен навсегда."
- f"{f'\n📄 Причина: {reason}' if reason else ''}"
- ),
+ text=(f"✅ Пользователь {tg_id} забанен навсегда.{f'\n📄 Причина: {reason}' if reason else ''}"),
reply_markup=build_editor_kb(tg_id, edit=True),
)
diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py
index 41ec239a..7ebea152 100644
--- a/handlers/keys/key_utils.py
+++ b/handlers/keys/key_utils.py
@@ -754,9 +754,9 @@ async def get_user_traffic(session: AsyncSession, tg_id: int, email: str) -> dic
server_id = list(server_ids)[0]
result = await session.execute(
- select(Server).where(
- (Server.server_name == server_id) | (Server.cluster_name == server_id)
- ).where(Server.enabled == True)
+ select(Server)
+ .where((Server.server_name == server_id) | (Server.cluster_name == server_id))
+ .where(Server.enabled is True)
)
server_rows = result.scalars().all()
if not server_rows:
diff --git a/handlers/refferal.py b/handlers/refferal.py
index 405d661d..29b26ad4 100644
--- a/handlers/refferal.py
+++ b/handlers/refferal.py
@@ -20,7 +20,15 @@ from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from bot import bot
-from config import ADMIN_ID, INLINE_MODE, REFERRAL_BONUS_PERCENTAGES, TOP_REFERRAL_BUTTON, TRIAL_CONFIG, USERNAME_BOT, REFERRAL_QR
+from config import (
+ ADMIN_ID,
+ INLINE_MODE,
+ REFERRAL_BONUS_PERCENTAGES,
+ REFERRAL_QR,
+ TOP_REFERRAL_BUTTON,
+ TRIAL_CONFIG,
+ USERNAME_BOT,
+)
from database import (
add_referral,
add_user,
diff --git a/middlewares/__init__.py b/middlewares/__init__.py
index 1375514e..f091229e 100644
--- a/middlewares/__init__.py
+++ b/middlewares/__init__.py
@@ -8,12 +8,12 @@ from middlewares.ban_checker import BanCheckerMiddleware
from middlewares.subscription import SubscriptionMiddleware
from .admin import AdminMiddleware
+from .direct_start_blocker import DirectStartBlockerMiddleware
from .loggings import LoggingMiddleware
from .maintenance import MaintenanceModeMiddleware
from .session import SessionMiddleware
from .throttling import ThrottlingMiddleware
from .user import UserMiddleware
-from .direct_start_blocker import DirectStartBlockerMiddleware
def register_middleware(
@@ -57,4 +57,3 @@ def register_middleware(
for handler in handlers:
handler.outer_middleware(middleware)
-
diff --git a/middlewares/direct_start_blocker.py b/middlewares/direct_start_blocker.py
index 493b66c1..122bb308 100644
--- a/middlewares/direct_start_blocker.py
+++ b/middlewares/direct_start_blocker.py
@@ -5,7 +5,7 @@ from aiogram import BaseMiddleware
from aiogram.types import Message, Update
from config import DISABLE_DIRECT_START
-from database import check_user_exists, async_session_maker
+from database import async_session_maker, check_user_exists
from logger import logger
diff --git a/web/__init__.py b/web/__init__.py
index f4ed9945..e83685c7 100644
--- a/web/__init__.py
+++ b/web/__init__.py
@@ -4,10 +4,10 @@ import bot
from config import TBLOCKER_WEBHOOK_PATH
+from .heleket_payment import heleket_payment_webhook
+from .kassai_payment import kassai_payment_webhook
from .tblocker import tblocker_webhook
from .wata_payment import wata_payment_webhook
-from .kassai_payment import kassai_payment_webhook
-from .heleket_payment import heleket_payment_webhook
WATA_WEBHOOK_PATH = "/wata/webhook"
diff --git a/web/heleket_payment.py b/web/heleket_payment.py
index fd80fddf..7e639a7e 100644
--- a/web/heleket_payment.py
+++ b/web/heleket_payment.py
@@ -1,12 +1,15 @@
-import hashlib
import base64
+import hashlib
import json
+
from aiohttp import web
+
+from config import HELEKET_API_KEY, HELEKET_CURRENCY_RATE
from database import add_payment, async_session_maker, update_balance
from handlers.payments.utils import send_payment_success_notification
-from config import HELEKET_API_KEY, HELEKET_CURRENCY_RATE
from logger import logger
+
processed_payments = set()
@@ -16,40 +19,42 @@ async def heleket_payment_webhook(request: web.Request):
"""
try:
data = await request.json()
-
+
logger.info(f"Heleket webhook received from {request.remote}")
logger.info(f"Heleket webhook data: {data}")
-
+
signature = data.get("sign", "")
if not verify_heleket_webhook_signature(data, signature):
logger.error("Heleket: Invalid signature")
return web.Response(status=400, text="Invalid signature")
-
+
uuid = data.get("uuid")
order_id = data.get("order_id")
payment_status = data.get("status")
- amount = data.get("amount")
- payment_amount = data.get("payment_amount")
+ amount = data.get("amount")
+ payment_amount = data.get("payment_amount")
additional_data = data.get("additional_data", "")
-
- logger.info(f"Heleket payment: uuid={uuid}, order_id={order_id}, status={payment_status}, amount={amount}, payment_amount={payment_amount}")
-
+
+ logger.info(
+ f"Heleket payment: uuid={uuid}, order_id={order_id}, status={payment_status}, amount={amount}, payment_amount={payment_amount}"
+ )
+
if payment_status != "paid":
logger.info(f"Heleket: Payment not completed, status={payment_status}")
return web.Response(status=200, text="OK")
-
+
if not amount or not uuid:
- logger.error(f"Heleket: Missing amount or uuid")
+ logger.error("Heleket: Missing amount or uuid")
return web.Response(status=400, text="Missing required fields")
-
+
if uuid in processed_payments:
logger.warning(f"Heleket: Duplicate payment uuid={uuid}")
return web.Response(status=200, text="OK")
-
+
if order_id and order_id in processed_payments:
logger.warning(f"Heleket: Duplicate payment order_id={order_id}")
return web.Response(status=200, text="OK")
-
+
tg_id = None
rub_amount = None
try:
@@ -60,35 +65,35 @@ async def heleket_payment_webhook(request: web.Request):
tg_id = int(part.split("tg_id:")[1])
elif part.startswith("rub_amount:"):
rub_amount = float(part.split("rub_amount:")[1])
-
+
elif order_id and "_" in order_id:
tg_id = int(order_id.split("_")[1])
usd_amount = float(amount)
rub_amount = usd_amount * HELEKET_CURRENCY_RATE
else:
- logger.error(f"Heleket: Cannot extract tg_id from data")
+ logger.error("Heleket: Cannot extract tg_id from data")
return web.Response(status=400, text="Cannot extract user ID")
-
+
except (ValueError, IndexError) as e:
logger.error(f"Heleket: Error extracting tg_id or rub_amount: {e}")
return web.Response(status=400, text="Invalid user ID or amount format")
-
+
if not rub_amount:
- logger.error(f"Heleket: Could not determine rub_amount")
+ logger.error("Heleket: Could not determine rub_amount")
return web.Response(status=400, text="Cannot determine amount")
-
+
async with async_session_maker() as session:
await update_balance(session, tg_id, rub_amount)
await send_payment_success_notification(tg_id, rub_amount, session)
await add_payment(session, tg_id, rub_amount, "heleket")
-
+
processed_payments.add(uuid)
if order_id:
processed_payments.add(order_id)
-
+
logger.info(f"✅ Heleket: Payment processed for user {tg_id}, amount {rub_amount} RUB (${amount}), uuid={uuid}")
return web.Response(status=200, text="OK")
-
+
except Exception as e:
logger.error(f"Heleket webhook error: {e}")
return web.Response(status=500, text="Internal server error")
@@ -100,22 +105,22 @@ def verify_heleket_webhook_signature(data: dict, signature: str) -> bool:
"""
try:
data_without_sign = {k: v for k, v in data.items() if k != "sign"}
-
- json_data = json.dumps(data_without_sign, separators=(',', ':'))
- base64_data = base64.b64encode(json_data.encode('utf-8')).decode('utf-8')
+
+ json_data = json.dumps(data_without_sign, separators=(",", ":"))
+ base64_data = base64.b64encode(json_data.encode("utf-8")).decode("utf-8")
sign_string = base64_data + HELEKET_API_KEY
- expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
-
+ expected_signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest()
+
result = signature.upper() == expected_signature.upper()
-
+
if not result:
- logger.error(f"Heleket webhook signature mismatch")
+ logger.error("Heleket webhook signature mismatch")
logger.error(f"Expected: {expected_signature}, Got: {signature}")
logger.error(f"Base64 data: {base64_data}")
logger.error(f"Sign string: {sign_string}")
-
+
return result
-
+
except Exception as e:
logger.error(f"Heleket signature verification error: {e}")
- return False
\ No newline at end of file
+ return False
diff --git a/web/kassai_payment.py b/web/kassai_payment.py
index 664b6e1f..aba8c53d 100644
--- a/web/kassai_payment.py
+++ b/web/kassai_payment.py
@@ -1,12 +1,15 @@
import hashlib
import json
+
from aiohttp import web
-from database import add_payment, async_session_maker, update_balance
-from handlers.payments.utils import send_payment_success_notification
-from handlers.payments.kassai import verify_kassai_signature
+
from config import KASSAI_SECRET_KEY, KASSAI_SHOP_ID
+from database import add_payment, async_session_maker, update_balance
+from handlers.payments.kassai import verify_kassai_signature
+from handlers.payments.utils import send_payment_success_notification
from logger import logger
+
processed_payments = set()
@@ -17,61 +20,63 @@ async def kassai_payment_webhook(request: web.Request):
try:
data = await request.post()
data_dict = dict(data)
-
+
logger.info(f"KassaAI webhook received from {request.remote}")
-
+
signature = data_dict.get("SIGN")
if not signature:
logger.error("KassaAI: Missing SIGN in request")
return web.Response(status=400, text="Signature missing")
-
+
if not verify_kassai_webhook_signature(data_dict, signature):
logger.error("KassaAI: Invalid signature")
return web.Response(status=400, text="Invalid signature")
-
+
merchant_order_id = data_dict.get("MERCHANT_ORDER_ID")
amount = data_dict.get("AMOUNT")
p_email = data_dict.get("P_EMAIL")
intid = data_dict.get("intid")
-
+
logger.info(f"KassaAI payment: intid={intid}, MERCHANT_ORDER_ID={merchant_order_id}, amount={amount}")
-
+
if not amount or not intid:
- logger.error(f"KassaAI: Missing AMOUNT or intid")
+ logger.error("KassaAI: Missing AMOUNT or intid")
return web.Response(status=400, text="Missing required fields")
-
+
if intid in processed_payments:
logger.warning(f"KassaAI: Duplicate payment intid={intid}")
return web.Response(status=200, text="YES")
-
+
if merchant_order_id and merchant_order_id in processed_payments:
logger.warning(f"KassaAI: Duplicate payment MERCHANT_ORDER_ID={merchant_order_id}")
return web.Response(status=200, text="YES")
-
+
try:
if p_email and "@" in p_email:
tg_id = int(p_email.split("@")[0])
else:
logger.error(f"KassaAI: Invalid email format: {p_email}")
return web.Response(status=400, text="Invalid email format")
-
+
amount_float = float(amount)
except (ValueError, TypeError) as e:
logger.error(f"KassaAI: Invalid data format: {e}")
return web.Response(status=400, text="Invalid data format")
-
+
async with async_session_maker() as session:
await update_balance(session, tg_id, amount_float)
await send_payment_success_notification(tg_id, amount_float, session)
await add_payment(session, tg_id, amount_float, "kassai")
-
+
processed_payments.add(intid)
if merchant_order_id:
processed_payments.add(merchant_order_id)
-
- logger.info(f"✅ KassaAI: Payment processed for user {tg_id}, amount {amount_float}, intid={intid}, MERCHANT_ORDER_ID={merchant_order_id}")
+
+ logger.info(
+ f"✅ KassaAI: Payment processed for user {tg_id}, amount {amount_float}, intid={intid}, MERCHANT_ORDER_ID={merchant_order_id}"
+ )
return web.Response(status=200, text="YES")
-
+
except Exception as e:
logger.error(f"KassaAI webhook error: {e}")
return web.Response(status=500, text="Internal server error")
@@ -88,15 +93,15 @@ def verify_kassai_webhook_signature(data: dict, signature: str) -> bool:
f"{KASSAI_SECRET_KEY}:"
f"{data.get('MERCHANT_ORDER_ID', '')}"
)
-
- expected_signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
+
+ expected_signature = hashlib.md5(sign_string.encode("utf-8")).hexdigest()
result = signature.upper() == expected_signature.upper()
-
+
if not result:
- logger.error(f"KassaAI webhook signature mismatch")
-
+ logger.error("KassaAI webhook signature mismatch")
+
return result
-
+
except Exception as e:
logger.error(f"KassaAI signature verification error: {e}")
- return False
\ No newline at end of file
+ return False