refactoring/utm/fix minor bugs
This commit is contained in:
+33
-8
@@ -30,14 +30,6 @@ BEGIN
|
||||
END IF;
|
||||
END$$;
|
||||
|
||||
UPDATE users
|
||||
SET balance = c.balance,
|
||||
trial = c.trial
|
||||
FROM connections c
|
||||
WHERE users.tg_id = c.tg_id;
|
||||
|
||||
DROP TABLE IF EXISTS connections;
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS payments
|
||||
(
|
||||
@@ -186,3 +178,36 @@ CREATE TABLE IF NOT EXISTS blocked_users (
|
||||
tg_id BIGINT PRIMARY KEY,
|
||||
blocked_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tracking_sources (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code TEXT UNIQUE NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT,
|
||||
is_active BOOLEAN DEFAULT TRUE
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS source_code TEXT REFERENCES tracking_sources (code);
|
||||
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'connections'
|
||||
) THEN
|
||||
EXECUTE $upd$
|
||||
UPDATE users
|
||||
SET balance = c.balance,
|
||||
trial = c.trial
|
||||
FROM connections c
|
||||
WHERE users.tg_id = c.tg_id;
|
||||
$upd$;
|
||||
|
||||
EXECUTE 'DROP TABLE connections';
|
||||
END IF;
|
||||
END$$;
|
||||
+56
-5
@@ -232,6 +232,7 @@ async def add_user(
|
||||
language_code: str = None,
|
||||
is_bot: bool = False,
|
||||
session: Any = None,
|
||||
source_code: str = None,
|
||||
):
|
||||
"""
|
||||
Добавляет нового пользователя в таблицу users.
|
||||
@@ -239,13 +240,13 @@ async def add_user(
|
||||
try:
|
||||
await session.execute(
|
||||
"""
|
||||
INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
INSERT INTO users (tg_id, username, first_name, last_name, language_code, is_bot, source_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (tg_id) DO NOTHING
|
||||
""",
|
||||
tg_id, username, first_name, last_name, language_code, is_bot,
|
||||
tg_id, username, first_name, last_name, language_code, is_bot, source_code
|
||||
)
|
||||
logger.info(f"[DB] Новый пользователь добавлен: {tg_id}")
|
||||
logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})")
|
||||
except Exception as e:
|
||||
logger.error(f"[DB] Ошибка при добавлении пользователя {tg_id}: {e}")
|
||||
raise
|
||||
@@ -1400,4 +1401,54 @@ async def check_notifications_bulk(notification_type: str, hours: int, session:
|
||||
]
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при массовой проверке уведомлений типа {notification_type}: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
|
||||
async def create_tracking_source(name: str, code: str, type_: str, created_by: int, session):
|
||||
await session.execute(
|
||||
"""
|
||||
INSERT INTO tracking_sources (name, code, type, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
""",
|
||||
name, code, type_, created_by
|
||||
)
|
||||
|
||||
|
||||
async def get_all_tracking_sources(session) -> list[dict]:
|
||||
records = await session.fetch("""
|
||||
SELECT
|
||||
ts.code,
|
||||
ts.name,
|
||||
ts.created_at,
|
||||
COUNT(DISTINCT u.tg_id) AS registrations,
|
||||
COUNT(DISTINCT CASE WHEN u.trial = 1 THEN u.tg_id END) AS trials,
|
||||
COUNT(DISTINCT CASE WHEN p.status = 'success' THEN p.tg_id END) AS payments
|
||||
FROM tracking_sources ts
|
||||
LEFT JOIN users u ON u.source_code = ts.code
|
||||
LEFT JOIN payments p ON p.tg_id = u.tg_id
|
||||
GROUP BY ts.code, ts.name, ts.created_at
|
||||
ORDER BY ts.created_at DESC
|
||||
""")
|
||||
return [dict(r) for r in records]
|
||||
|
||||
|
||||
async def get_tracking_source_stats(code: str, session) -> dict:
|
||||
result = await session.fetchrow("""
|
||||
SELECT
|
||||
ts.name,
|
||||
ts.code,
|
||||
ts.created_at,
|
||||
COUNT(DISTINCT u.tg_id) AS registrations,
|
||||
COUNT(DISTINCT CASE WHEN u.trial = 1 THEN u.tg_id END) AS trials,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN p.status = 'success' AND NOT EXISTS (
|
||||
SELECT 1 FROM keys k WHERE k.tg_id = u.tg_id
|
||||
) THEN u.tg_id
|
||||
END) AS payments
|
||||
FROM tracking_sources ts
|
||||
LEFT JOIN users u ON u.source_code = ts.code
|
||||
LEFT JOIN payments p ON p.tg_id = u.tg_id
|
||||
WHERE ts.code = $1
|
||||
GROUP BY ts.code, ts.name, ts.created_at
|
||||
""", code)
|
||||
return dict(result) if result else {}
|
||||
|
||||
@@ -13,6 +13,7 @@ from .pay import router as pay_router
|
||||
from .payments import router as payments_router
|
||||
from .profile import router as profile_router
|
||||
from .start import router as start_router
|
||||
from .refferal import router as refferal_router
|
||||
|
||||
|
||||
router = Router(name="handlers_main_router")
|
||||
@@ -29,4 +30,5 @@ router.include_routers(
|
||||
keys_router,
|
||||
instructions_router,
|
||||
admin_router,
|
||||
refferal_router
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from .sender import router as sender_router
|
||||
from .servers import router as servers_router
|
||||
from .stats import router as stats_router
|
||||
from .users import router as users_router
|
||||
from .ads import router as ads_router
|
||||
|
||||
|
||||
router = Router(name="admins_main_router")
|
||||
@@ -29,4 +30,5 @@ router.include_routers(
|
||||
coupons_router,
|
||||
restart_router,
|
||||
bans_router,
|
||||
ads_router
|
||||
)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from .ads_handler import router
|
||||
@@ -0,0 +1,151 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from config import USERNAME_BOT
|
||||
from database import (
|
||||
create_tracking_source,
|
||||
get_all_tracking_sources,
|
||||
get_tracking_source_stats,
|
||||
)
|
||||
from filters.admin import IsAdminFilter
|
||||
from logger import logger
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback
|
||||
from .keyboard import (
|
||||
AdminAdsCallback,
|
||||
build_ads_kb,
|
||||
build_ads_list_kb,
|
||||
build_ads_stats_kb,
|
||||
build_ads_delete_confirm_kb,
|
||||
)
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class AdminAdsState(StatesGroup):
|
||||
waiting_for_new_name = State()
|
||||
waiting_for_new_code = State()
|
||||
|
||||
|
||||
@router.callback_query(AdminPanelCallback.filter(F.action == "ads"), IsAdminFilter())
|
||||
async def handle_ads_menu(callback_query: CallbackQuery):
|
||||
await callback_query.message.edit_text(
|
||||
text="📊 <b>Аналитика рекламы:</b>", reply_markup=build_ads_kb()
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(AdminAdsCallback.filter(F.action == "create"), IsAdminFilter())
|
||||
async def handle_ads_create(callback_query: CallbackQuery, state: FSMContext):
|
||||
await state.set_state(AdminAdsState.waiting_for_new_name)
|
||||
await callback_query.message.edit_text("📝 Введите <b>название</b> новой ссылки:")
|
||||
|
||||
|
||||
@router.message(AdminAdsState.waiting_for_new_name, IsAdminFilter())
|
||||
async def handle_ads_name_input(message: Message, state: FSMContext):
|
||||
name = message.text.strip()
|
||||
await state.update_data(name=name)
|
||||
await state.set_state(AdminAdsState.waiting_for_new_code)
|
||||
await message.answer(f"🔗 Введите <b>код ссылки</b> для: <code>{name}</code>.")
|
||||
|
||||
|
||||
@router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter())
|
||||
async def handle_ads_code_input(message: Message, state: FSMContext, session):
|
||||
code = message.text.strip()
|
||||
data = await state.get_data()
|
||||
name = data["name"]
|
||||
code_with_prefix = f"utm_{code}"
|
||||
|
||||
try:
|
||||
await create_tracking_source(
|
||||
name=name,
|
||||
code=code_with_prefix,
|
||||
type_="utm",
|
||||
created_by=message.from_user.id,
|
||||
session=session
|
||||
)
|
||||
stats = await get_tracking_source_stats(code_with_prefix, session)
|
||||
if not stats:
|
||||
await message.answer("❌ Источник не найден или не содержит данных.")
|
||||
return
|
||||
msg = format_ads_stats(stats, USERNAME_BOT)
|
||||
await message.answer(
|
||||
text=msg,
|
||||
reply_markup=build_ads_stats_kb(code_with_prefix),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании ссылки: {e}")
|
||||
await message.answer("❌ Произошла ошибка при создании ссылки.")
|
||||
finally:
|
||||
await state.clear()
|
||||
|
||||
|
||||
|
||||
@router.callback_query(AdminAdsCallback.filter(F.action == "list"), IsAdminFilter())
|
||||
async def handle_ads_list(callback_query: CallbackQuery, session):
|
||||
try:
|
||||
ads = await get_all_tracking_sources(session)
|
||||
reply_markup = build_ads_list_kb(ads, current_page=1, total_pages=1)
|
||||
await callback_query.message.edit_text("📋 Выберите ссылку для просмотра статистики:", reply_markup=reply_markup)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении списка UTM: {e}")
|
||||
await callback_query.message.edit_text("Произошла ошибка при получении списка.")
|
||||
|
||||
|
||||
@router.callback_query(AdminAdsCallback.filter(F.action == "view"), IsAdminFilter())
|
||||
async def handle_ads_view(callback_query: CallbackQuery, callback_data: AdminAdsCallback, session):
|
||||
code = callback_data.code
|
||||
try:
|
||||
stats = await get_tracking_source_stats(code, session)
|
||||
if not stats:
|
||||
await callback_query.message.edit_text("❌ Источник не найден или не содержит данных.")
|
||||
return
|
||||
msg = format_ads_stats(stats, USERNAME_BOT)
|
||||
await callback_query.message.edit_text(text=msg, reply_markup=build_ads_stats_kb(code), parse_mode=ParseMode.HTML)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при просмотре статистики: {e}")
|
||||
await callback_query.message.edit_text("❌ Ошибка при получении статистики.")
|
||||
|
||||
|
||||
@router.callback_query(AdminAdsCallback.filter(F.action == "delete_confirm"), IsAdminFilter())
|
||||
async def handle_ads_delete_confirm(callback_query: CallbackQuery, callback_data: AdminAdsCallback):
|
||||
code = callback_data.code
|
||||
await callback_query.message.edit_text(
|
||||
text=f"Вы уверены, что хотите удалить ссылку <code>{code}</code>?",
|
||||
reply_markup=build_ads_delete_confirm_kb(code),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(AdminAdsCallback.filter(F.action == "delete"), IsAdminFilter())
|
||||
async def handle_ads_delete(callback_query: CallbackQuery, callback_data: AdminAdsCallback, session):
|
||||
code = callback_data.code
|
||||
try:
|
||||
await session.execute("DELETE FROM tracking_sources WHERE code = $1", code)
|
||||
await callback_query.message.edit_text(
|
||||
f"🗑️ Ссылка <code>{code}</code> удалена.",
|
||||
reply_markup=build_ads_kb()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении ссылки {code}: {e}")
|
||||
await callback_query.message.edit_text("❌ Не удалось удалить ссылку.")
|
||||
|
||||
|
||||
def format_ads_stats(stats: dict, username_bot: str) -> str:
|
||||
return (
|
||||
f"<b>📊 <u>Статистика по рекламной ссылке</u></b>\n\n"
|
||||
f"📌 <b>Название:</b> {stats['name']}\n"
|
||||
f"🔗 <b>Ссылка:</b> <code>https://t.me/{username_bot}?start={stats['code']}</code>\n"
|
||||
f"🕓 <b>Создана:</b> {stats['created_at'].strftime('%d.%m.%Y %H:%M')}\n\n"
|
||||
|
||||
f"💡 <b>Активность:</b>\n"
|
||||
f"└ 🆕 <b>Регистраций:</b> <b>{stats.get('registrations', 0)}</b>\n"
|
||||
f"└ 🧪 <b>Триалов:</b> <b>{stats.get('trials', 0)}</b>\n"
|
||||
|
||||
f"\n💰 <b>Финансовая информация:</b>\n"
|
||||
f"└ 💳 <b>Покупок:</b> <b>{stats.get('payments', 0)}</b>\n\n"
|
||||
|
||||
f"<i>Просмотр статистики и управление рекламными ссылками</i>."
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
|
||||
from ..panel.keyboard import build_admin_back_btn
|
||||
|
||||
|
||||
class AdminAdsCallback(CallbackData, prefix="admin_ads"):
|
||||
action: str
|
||||
code: str | None = None
|
||||
|
||||
def build_ads_kb() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="➕ Новая ссылка", callback_data=AdminAdsCallback(action="create").pack())
|
||||
builder.button(text="📊 Список", callback_data=AdminAdsCallback(action="list").pack())
|
||||
builder.row(build_admin_back_btn())
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_ads_list_kb(ads: list, current_page: int, total_pages: int) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for ad in ads:
|
||||
builder.button(
|
||||
text=f"📎 {ad['name']}",
|
||||
callback_data=AdminAdsCallback(action="view", code=ad["code"]).pack(),
|
||||
)
|
||||
|
||||
pagination_buttons = []
|
||||
if current_page > 1:
|
||||
pagination_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=AdminAdsCallback(action="list", code=f"{current_page - 1}").pack(),
|
||||
)
|
||||
)
|
||||
if current_page < total_pages:
|
||||
pagination_buttons.append(
|
||||
InlineKeyboardButton(
|
||||
text="Вперед ➡️",
|
||||
callback_data=AdminAdsCallback(action="list", code=f"{current_page + 1}").pack(),
|
||||
)
|
||||
)
|
||||
if pagination_buttons:
|
||||
builder.row(*pagination_buttons)
|
||||
|
||||
builder.row(build_admin_back_btn("ads"))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_ads_stats_kb(code: str) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🗑️ Удалить", callback_data=AdminAdsCallback(action="delete_confirm", code=code).pack())
|
||||
builder.row(build_admin_back_btn("ads"))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_ads_delete_confirm_kb(code: str) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(
|
||||
text="✅ Да, удалить",
|
||||
callback_data=AdminAdsCallback(action="delete", code=code,).pack()
|
||||
)
|
||||
builder.button(
|
||||
text="❌ Отмена",
|
||||
callback_data=AdminAdsCallback(action="view", code=code).pack()
|
||||
)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
@@ -23,15 +23,19 @@ def build_panel_kb() -> InlineKeyboardMarkup:
|
||||
builder.button(text="🔑 Поиск по названию ключа", callback_data=AdminPanelCallback(action="search_key").pack())
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="🖥️ Серверы", callback_data=AdminPanelCallback(action="clusters").pack()),
|
||||
InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
|
||||
)
|
||||
builder.button(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack())
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data=AdminPanelCallback(action="stats").pack()),
|
||||
InlineKeyboardButton(text="🤖 Управление", callback_data=AdminPanelCallback(action="management").pack()),
|
||||
)
|
||||
builder.row(
|
||||
|
||||
InlineKeyboardButton(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()),
|
||||
InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📊 Статистика", callback_data=AdminPanelCallback(action="stats").pack()),
|
||||
InlineKeyboardButton(text="📈 Аналитика", callback_data=AdminPanelCallback(action="ads").pack()),
|
||||
)
|
||||
builder.button(text=MAIN_MENU, callback_data="profile")
|
||||
builder.adjust(1, 1, 2, 1, 2, 1)
|
||||
builder.adjust(1, 1, 2, 2, 2, 1)
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ from handlers.keys.key_utils import (
|
||||
reset_traffic_in_cluster,
|
||||
update_subscription,
|
||||
)
|
||||
from handlers.utils import generate_random_email
|
||||
from handlers.utils import generate_random_email, sanitize_key_name
|
||||
from logger import logger
|
||||
from utils.csv_export import export_referrals_csv
|
||||
|
||||
@@ -98,6 +98,24 @@ async def handle_search_key(callback_query: CallbackQuery, state: FSMContext):
|
||||
await callback_query.message.edit_text(text="🔑 Введите имя ключа для поиска:", reply_markup=build_admin_back_kb())
|
||||
|
||||
|
||||
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
|
||||
async def handle_key_name_input(message: Message, state: FSMContext, session: Any):
|
||||
kb = build_admin_back_kb()
|
||||
|
||||
if not message.text:
|
||||
await message.answer(text="🚫 Пожалуйста, отправьте текстовое сообщение.", reply_markup=kb)
|
||||
return
|
||||
|
||||
key_name = sanitize_key_name(message.text)
|
||||
key_details = await get_key_details(key_name, session)
|
||||
|
||||
if not key_details:
|
||||
await message.answer(text="🚫 Пользователь с указанным именем ключа не найден.", reply_markup=kb)
|
||||
return
|
||||
|
||||
await process_user_search(message, state, session, key_details["tg_id"])
|
||||
|
||||
|
||||
@router.message(UserEditorState.waiting_for_user_data, IsAdminFilter())
|
||||
async def handle_user_data_input(message: Message, state: FSMContext, session: Any):
|
||||
kb = build_admin_back_kb()
|
||||
|
||||
@@ -111,8 +111,6 @@ async def activate_coupon(message: Message, state: FSMContext, session: Any, cou
|
||||
await update_coupon_usage_count(coupon_record["id"], session)
|
||||
await create_coupon_usage(coupon_record["id"], user_id, session)
|
||||
await message.answer(f"✅ Купон активирован, на баланс начислено {coupon_record['amount']} рублей.")
|
||||
is_admin = message.from_user.id in ADMIN_ID
|
||||
await process_callback_view_profile(message, state, admin=is_admin)
|
||||
await state.clear()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при активации купона на баланс: {e}")
|
||||
|
||||
+65
-2
@@ -1,6 +1,8 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from typing import Any
|
||||
import os
|
||||
|
||||
from config import (
|
||||
CRYPTO_BOT_ENABLE,
|
||||
@@ -10,8 +12,9 @@ from config import (
|
||||
YOOKASSA_ENABLE,
|
||||
YOOMONEY_ENABLE,
|
||||
)
|
||||
from handlers.buttons import CRYPTOBOT, MAIN_MENU, ROBOKASSA, STARS, YOOKASSA, YOOMONEY
|
||||
from handlers.texts import PAYMENT_METHODS_MSG
|
||||
from handlers.buttons import CRYPTOBOT, MAIN_MENU, ROBOKASSA, STARS, YOOKASSA, YOOMONEY, PAYMENT, BALANCE_HISTORY, COUPON
|
||||
from handlers.texts import PAYMENT_METHODS_MSG, BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT
|
||||
from database import get_last_payments
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
@@ -69,3 +72,63 @@ async def handle_pay(callback_query: CallbackQuery):
|
||||
media_path=None,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "balance")
|
||||
async def balance_handler(callback_query: CallbackQuery, session: Any):
|
||||
result = await session.fetchrow(
|
||||
"SELECT balance FROM users WHERE tg_id = $1",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
balance = result["balance"] if result else 0.0
|
||||
balance = int(balance)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
|
||||
builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history"))
|
||||
builder.row(InlineKeyboardButton(text=COUPON, callback_data="activate_coupon"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
text = BALANCE_MANAGEMENT_TEXT.format(balance=balance)
|
||||
image_path = os.path.join("img", "pic.jpg")
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=text,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=image_path,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "balance_history")
|
||||
async def balance_history_handler(callback_query: CallbackQuery, session: Any):
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
records = await get_last_payments(callback_query.from_user.id, session)
|
||||
|
||||
if records:
|
||||
history_text = BALANCE_HISTORY_HEADER
|
||||
for record in records:
|
||||
amount = record["amount"]
|
||||
payment_system = record["payment_system"]
|
||||
status = record["status"]
|
||||
date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
history_text += (
|
||||
f"<b>Сумма:</b> {amount}₽\n"
|
||||
f"<b>Способ оплаты:</b> {payment_system}\n"
|
||||
f"<b>Статус:</b> {status}\n"
|
||||
f"<b>Дата:</b> {date}\n\n"
|
||||
)
|
||||
else:
|
||||
history_text = "❌ У вас пока нет операций с балансом."
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=history_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
Binary file not shown.
+3
-244
@@ -1,62 +1,42 @@
|
||||
import html
|
||||
import os
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
import qrcode
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
InlineQuery,
|
||||
InlineQueryResultArticle,
|
||||
InputTextMessageContent,
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
ADMIN_ID,
|
||||
DATABASE_URL,
|
||||
GIFT_BUTTON,
|
||||
INLINE_MODE,
|
||||
INSTRUCTIONS_BUTTON,
|
||||
NEWS_MESSAGE,
|
||||
REFERRAL_BUTTON,
|
||||
SHOW_START_MENU_ONCE,
|
||||
TOP_REFERRAL_BUTTON,
|
||||
TRIAL_TIME,
|
||||
USERNAME_BOT,
|
||||
)
|
||||
from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
|
||||
from database import get_balance, get_key_count, get_trial
|
||||
from handlers.buttons import (
|
||||
ABOUT_VPN,
|
||||
ADD_SUB,
|
||||
BACK,
|
||||
BALANCE,
|
||||
BALANCE_HISTORY,
|
||||
COUPON,
|
||||
GIFTS,
|
||||
INSTRUCTIONS,
|
||||
INVITE,
|
||||
MAIN_MENU,
|
||||
MY_SUBS,
|
||||
PAYMENT,
|
||||
QR,
|
||||
TOP_FIVE,
|
||||
TRIAL_SUB,
|
||||
)
|
||||
from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT, REFERRAL_OFFERS
|
||||
from logger import logger
|
||||
|
||||
from .admin.panel.keyboard import AdminPanelCallback
|
||||
from .texts import get_referral_link, invite_message_send, profile_message_send
|
||||
from .utils import edit_or_send_message, format_days
|
||||
from .texts import profile_message_send
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -147,224 +127,3 @@ async def process_callback_view_profile(
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
@router.callback_query(F.data == "balance")
|
||||
async def balance_handler(callback_query: CallbackQuery, session: Any):
|
||||
result = await session.fetchrow(
|
||||
"SELECT balance FROM users WHERE tg_id = $1",
|
||||
callback_query.from_user.id,
|
||||
)
|
||||
balance = result["balance"] if result else 0.0
|
||||
balance = int(balance)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
|
||||
builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history"))
|
||||
builder.row(InlineKeyboardButton(text=COUPON, callback_data="activate_coupon"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
text = BALANCE_MANAGEMENT_TEXT.format(balance=balance)
|
||||
image_path = os.path.join("img", "pic.jpg")
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=text,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=image_path,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "balance_history")
|
||||
async def balance_history_handler(callback_query: CallbackQuery, session: Any):
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=PAYMENT, callback_data="pay"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
records = await get_last_payments(callback_query.from_user.id, session)
|
||||
|
||||
if records:
|
||||
history_text = BALANCE_HISTORY_HEADER
|
||||
for record in records:
|
||||
amount = record["amount"]
|
||||
payment_system = record["payment_system"]
|
||||
status = record["status"]
|
||||
date = record["created_at"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
history_text += (
|
||||
f"<b>Сумма:</b> {amount}₽\n"
|
||||
f"<b>Способ оплаты:</b> {payment_system}\n"
|
||||
f"<b>Статус:</b> {status}\n"
|
||||
f"<b>Дата:</b> {date}\n\n"
|
||||
)
|
||||
else:
|
||||
history_text = "❌ У вас пока нет операций с балансом."
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=history_text,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "invite")
|
||||
@router.message(F.text == "/invite")
|
||||
async def invite_handler(callback_query_or_message: Message | CallbackQuery):
|
||||
chat_id = None
|
||||
if isinstance(callback_query_or_message, CallbackQuery):
|
||||
chat_id = callback_query_or_message.message.chat.id
|
||||
target_message = callback_query_or_message.message
|
||||
else:
|
||||
chat_id = callback_query_or_message.chat.id
|
||||
target_message = callback_query_or_message
|
||||
|
||||
referral_link = get_referral_link(chat_id)
|
||||
referral_stats = await get_referral_stats(chat_id)
|
||||
invite_message = invite_message_send(referral_link, referral_stats)
|
||||
image_path = os.path.join("img", "pic_invite.jpg")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
if INLINE_MODE:
|
||||
builder.button(text=INVITE, switch_inline_query="invite")
|
||||
else:
|
||||
invite_text = INVITE_TEXT_NON_INLINE.format(referral_link=referral_link)
|
||||
builder.button(text=INVITE, switch_inline_query=invite_text)
|
||||
builder.button(text=QR, callback_data=f"show_referral_qr|{chat_id}")
|
||||
if TOP_REFERRAL_BUTTON:
|
||||
builder.button(text=TOP_FIVE, callback_data="top_referrals")
|
||||
builder.button(text=MAIN_MENU, callback_data="profile")
|
||||
builder.adjust(1)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text=invite_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=image_path,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
|
||||
|
||||
@router.inline_query(F.query.in_(["referral", "ref", "invite"]))
|
||||
async def inline_referral_handler(inline_query: InlineQuery):
|
||||
referral_link = f"https://t.me/{USERNAME_BOT}?start=referral_{inline_query.from_user.id}"
|
||||
trial_time_formatted = format_days(TRIAL_TIME)
|
||||
results: list[InlineQueryResultArticle] = []
|
||||
|
||||
for index, offer in enumerate(REFERRAL_OFFERS):
|
||||
description = offer["description"][:64]
|
||||
message_text = offer["message"].format(trial_time=TRIAL_TIME, trial_time_formatted=trial_time_formatted)[:4096]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=offer["title"], url=referral_link))
|
||||
|
||||
results.append(
|
||||
InlineQueryResultArticle(
|
||||
id=str(index),
|
||||
title=offer["title"],
|
||||
description=description,
|
||||
input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
)
|
||||
|
||||
await inline_query.answer(results=results, cache_time=86400, is_personal=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("show_referral_qr|"))
|
||||
async def show_referral_qr(callback_query: CallbackQuery):
|
||||
try:
|
||||
chat_id = callback_query.data.split("|")[1]
|
||||
referral_link = get_referral_link(chat_id)
|
||||
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
||||
qr.add_data(referral_link)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
|
||||
qr_path = f"/tmp/qrcode_referral_{chat_id}.png"
|
||||
with open(qr_path, "wb") as f:
|
||||
f.write(buffer.read())
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="invite"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="📷 <b>Ваш QR-код для реферальной ссылки.</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=qr_path,
|
||||
)
|
||||
|
||||
os.remove(qr_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True)
|
||||
await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "top_referrals")
|
||||
async def top_referrals_handler(callback_query: CallbackQuery):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
user_referral_count = (
|
||||
await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", callback_query.from_user.id)
|
||||
or 0
|
||||
)
|
||||
|
||||
personal_block = "Твоё место в рейтинге:\n"
|
||||
if user_referral_count > 0:
|
||||
user_position = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*) + 1 FROM (
|
||||
SELECT COUNT(*) as cnt
|
||||
FROM referrals
|
||||
GROUP BY referrer_tg_id
|
||||
HAVING COUNT(*) > $1
|
||||
) AS better_users
|
||||
""",
|
||||
user_referral_count,
|
||||
)
|
||||
personal_block += f"{user_position}. {callback_query.from_user.id} - {user_referral_count} чел."
|
||||
else:
|
||||
personal_block += "Ты еще не приглашал пользователей в проект."
|
||||
|
||||
top_referrals = await conn.fetch(
|
||||
"""
|
||||
SELECT referrer_tg_id, COUNT(*) as referral_count
|
||||
FROM referrals
|
||||
GROUP BY referrer_tg_id
|
||||
ORDER BY referral_count DESC
|
||||
LIMIT 5
|
||||
"""
|
||||
)
|
||||
|
||||
is_admin = callback_query.from_user.id in ADMIN_ID
|
||||
rows = ""
|
||||
for i, row in enumerate(top_referrals, 1):
|
||||
tg_id = str(row["referrer_tg_id"])
|
||||
count = row["referral_count"]
|
||||
display_id = tg_id if is_admin else f"{tg_id[:5]}*****"
|
||||
rows += f"{i}. {display_id} - {count} чел.\n"
|
||||
|
||||
text = TOP_REFERRALS_TEXT.format(personal_block=personal_block, rows=rows)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="invite"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=text,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import os
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
import qrcode
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
InlineQuery,
|
||||
InlineQueryResultArticle,
|
||||
InputTextMessageContent,
|
||||
Message,
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
ADMIN_ID,
|
||||
DATABASE_URL,
|
||||
INLINE_MODE,
|
||||
TOP_REFERRAL_BUTTON,
|
||||
TRIAL_TIME,
|
||||
USERNAME_BOT,
|
||||
)
|
||||
from database import get_referral_stats, get_referral_by_referred_id, add_referral, add_user, check_user_exists
|
||||
from handlers.buttons import (
|
||||
BACK,
|
||||
INVITE,
|
||||
MAIN_MENU,
|
||||
QR,
|
||||
TOP_FIVE,
|
||||
)
|
||||
from handlers.texts import INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT, REFERRAL_OFFERS, REFERRAL_SUCCESS_MSG, NEW_REFERRAL_NOTIFICATION
|
||||
from logger import logger
|
||||
|
||||
from .texts import get_referral_link, invite_message_send
|
||||
from .utils import edit_or_send_message, format_days
|
||||
|
||||
from bot import bot
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
@router.callback_query(F.data == "invite")
|
||||
@router.message(F.text == "/invite")
|
||||
async def invite_handler(callback_query_or_message: Message | CallbackQuery):
|
||||
chat_id = None
|
||||
if isinstance(callback_query_or_message, CallbackQuery):
|
||||
chat_id = callback_query_or_message.message.chat.id
|
||||
target_message = callback_query_or_message.message
|
||||
else:
|
||||
chat_id = callback_query_or_message.chat.id
|
||||
target_message = callback_query_or_message
|
||||
|
||||
referral_link = get_referral_link(chat_id)
|
||||
referral_stats = await get_referral_stats(chat_id)
|
||||
invite_message = invite_message_send(referral_link, referral_stats)
|
||||
image_path = os.path.join("img", "pic_invite.jpg")
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
if INLINE_MODE:
|
||||
builder.button(text=INVITE, switch_inline_query="invite")
|
||||
else:
|
||||
invite_text = INVITE_TEXT_NON_INLINE.format(referral_link=referral_link)
|
||||
builder.button(text=INVITE, switch_inline_query=invite_text)
|
||||
builder.button(text=QR, callback_data=f"show_referral_qr|{chat_id}")
|
||||
if TOP_REFERRAL_BUTTON:
|
||||
builder.button(text=TOP_FIVE, callback_data="top_referrals")
|
||||
builder.button(text=MAIN_MENU, callback_data="profile")
|
||||
builder.adjust(1)
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=target_message,
|
||||
text=invite_message,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=image_path,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
|
||||
|
||||
@router.inline_query(F.query.in_(["referral", "ref", "invite"]))
|
||||
async def inline_referral_handler(inline_query: InlineQuery):
|
||||
referral_link = f"https://t.me/{USERNAME_BOT}?start=referral_{inline_query.from_user.id}"
|
||||
trial_time_formatted = format_days(TRIAL_TIME)
|
||||
results: list[InlineQueryResultArticle] = []
|
||||
|
||||
for index, offer in enumerate(REFERRAL_OFFERS):
|
||||
description = offer["description"][:64]
|
||||
message_text = offer["message"].format(trial_time=TRIAL_TIME, trial_time_formatted=trial_time_formatted)[:4096]
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=offer["title"], url=referral_link))
|
||||
|
||||
results.append(
|
||||
InlineQueryResultArticle(
|
||||
id=str(index),
|
||||
title=offer["title"],
|
||||
description=description,
|
||||
input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
)
|
||||
|
||||
await inline_query.answer(results=results, cache_time=86400, is_personal=True)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("show_referral_qr|"))
|
||||
async def show_referral_qr(callback_query: CallbackQuery):
|
||||
try:
|
||||
chat_id = callback_query.data.split("|")[1]
|
||||
referral_link = get_referral_link(chat_id)
|
||||
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=4)
|
||||
qr.add_data(referral_link)
|
||||
qr.make(fit=True)
|
||||
|
||||
img = qr.make_image(fill_color="black", back_color="white")
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="PNG")
|
||||
buffer.seek(0)
|
||||
|
||||
qr_path = f"/tmp/qrcode_referral_{chat_id}.png"
|
||||
with open(qr_path, "wb") as f:
|
||||
f.write(buffer.read())
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="invite"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text="📷 <b>Ваш QR-код для реферальной ссылки.</b>",
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=qr_path,
|
||||
)
|
||||
|
||||
os.remove(qr_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True)
|
||||
await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.")
|
||||
|
||||
|
||||
@router.callback_query(F.data == "top_referrals")
|
||||
async def top_referrals_handler(callback_query: CallbackQuery):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
user_referral_count = (
|
||||
await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", callback_query.from_user.id)
|
||||
or 0
|
||||
)
|
||||
|
||||
personal_block = "Твоё место в рейтинге:\n"
|
||||
if user_referral_count > 0:
|
||||
user_position = await conn.fetchval(
|
||||
"""
|
||||
SELECT COUNT(*) + 1 FROM (
|
||||
SELECT COUNT(*) as cnt
|
||||
FROM referrals
|
||||
GROUP BY referrer_tg_id
|
||||
HAVING COUNT(*) > $1
|
||||
) AS better_users
|
||||
""",
|
||||
user_referral_count,
|
||||
)
|
||||
personal_block += f"{user_position}. {callback_query.from_user.id} - {user_referral_count} чел."
|
||||
else:
|
||||
personal_block += "Ты еще не приглашал пользователей в проект."
|
||||
|
||||
top_referrals = await conn.fetch(
|
||||
"""
|
||||
SELECT referrer_tg_id, COUNT(*) as referral_count
|
||||
FROM referrals
|
||||
GROUP BY referrer_tg_id
|
||||
ORDER BY referral_count DESC
|
||||
LIMIT 5
|
||||
"""
|
||||
)
|
||||
|
||||
is_admin = callback_query.from_user.id in ADMIN_ID
|
||||
rows = ""
|
||||
for i, row in enumerate(top_referrals, 1):
|
||||
tg_id = str(row["referrer_tg_id"])
|
||||
count = row["referral_count"]
|
||||
display_id = tg_id if is_admin else f"{tg_id[:5]}*****"
|
||||
rows += f"{i}. {display_id} - {count} чел.\n"
|
||||
|
||||
text = TOP_REFERRALS_TEXT.format(personal_block=personal_block, rows=rows)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="invite"))
|
||||
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
|
||||
await edit_or_send_message(
|
||||
target_message=callback_query.message,
|
||||
text=text,
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def handle_referral_link(referral_code: str, message: Message, state: FSMContext, session: Any):
|
||||
try:
|
||||
referrer_tg_id = int(referral_code)
|
||||
user_exists_now = await check_user_exists(message.chat.id)
|
||||
|
||||
if referrer_tg_id == message.chat.id:
|
||||
await message.answer("❌ Вы не можете быть реферальной ссылкой самого себя.")
|
||||
return
|
||||
|
||||
if user_exists_now:
|
||||
await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.")
|
||||
return
|
||||
|
||||
existing_referral = await get_referral_by_referred_id(message.chat.id, session)
|
||||
if existing_referral:
|
||||
await message.answer("❌ Вы уже использовали реферальную ссылку.")
|
||||
return
|
||||
|
||||
await add_referral(message.chat.id, referrer_tg_id, session)
|
||||
|
||||
from_user = message.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
|
||||
try:
|
||||
await bot.send_message(
|
||||
referrer_tg_id,
|
||||
NEW_REFERRAL_NOTIFICATION.format(referred_id=message.chat.id),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить уведомление пригласившему ({referrer_tg_id}): {e}")
|
||||
|
||||
await message.answer(REFERRAL_SUCCESS_MSG.format(referrer_tg_id=referrer_tg_id))
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке реферальной ссылки {referral_code}: {e}")
|
||||
await message.answer("❌ Произошла ошибка при обработке реферальной ссылки.")
|
||||
return
|
||||
+114
-152
@@ -24,22 +24,17 @@ from config import (
|
||||
SUPPORT_CHAT_URL,
|
||||
)
|
||||
from database import (
|
||||
add_referral,
|
||||
get_referral_by_referred_id,
|
||||
get_trial,
|
||||
add_user,
|
||||
check_user_exists
|
||||
check_user_exists,
|
||||
)
|
||||
from handlers.payments.gift import handle_gift_link
|
||||
from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT, TRIAL_SUB
|
||||
from handlers.captcha import generate_captcha
|
||||
from handlers.coupons import activate_coupon
|
||||
from handlers.keys.key_mode.key_create import create_key
|
||||
from handlers.profile import process_callback_view_profile
|
||||
from handlers.texts import (
|
||||
GIFT_ALREADY_USED_OR_NOT_EXISTS_MSG,
|
||||
NEW_REFERRAL_NOTIFICATION,
|
||||
NOT_SUBSCRIBED_YET_MSG,
|
||||
REFERRAL_SUCCESS_MSG,
|
||||
SUBSCRIPTION_CHECK_ERROR_MSG,
|
||||
SUBSCRIPTION_CONFIRMED_MSG,
|
||||
SUBSCRIPTION_REQUIRED_MSG,
|
||||
@@ -50,6 +45,7 @@ from logger import logger
|
||||
|
||||
from .admin.panel.keyboard import AdminPanelCallback
|
||||
from .utils import edit_or_send_message
|
||||
from .refferal import handle_referral_link
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -111,151 +107,6 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
|
||||
await process_start_logic(message, state, session, admin, text_to_process)
|
||||
|
||||
|
||||
async def process_start_logic(
|
||||
message: Message, state: FSMContext, session: Any, admin: bool, text_to_process: str = None
|
||||
):
|
||||
text = text_to_process if text_to_process is not None else message.text
|
||||
if text:
|
||||
try:
|
||||
if "coupons_" in text:
|
||||
logger.info(f"Обнаружена ссылка на купон: {text}")
|
||||
coupon_code = text.split("coupons_")[1]
|
||||
await activate_coupon(message, state, session, coupon_code=coupon_code, admin=admin)
|
||||
return
|
||||
|
||||
if "gift_" in text:
|
||||
parts = text.split("gift_")[1].split("_")
|
||||
if len(parts) < 2:
|
||||
await message.answer("❌ Неверный формат ссылки на подарок.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
gift_id = parts[0]
|
||||
async with session.transaction():
|
||||
gift_info = await session.fetchrow(
|
||||
"""
|
||||
SELECT sender_tg_id, selected_months, expiry_time, is_used, recipient_tg_id
|
||||
FROM gifts
|
||||
WHERE gift_id = $1
|
||||
FOR UPDATE
|
||||
""",
|
||||
gift_id,
|
||||
)
|
||||
if not gift_info:
|
||||
await message.answer(GIFT_ALREADY_USED_OR_NOT_EXISTS_MSG)
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
|
||||
if gift_info["is_used"]:
|
||||
await message.answer("Этот подарок уже был использован.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
|
||||
if gift_info["sender_tg_id"] == message.chat.id:
|
||||
await message.answer("❌ Вы не можете получить подарок от самого себя.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
|
||||
if gift_info["recipient_tg_id"]:
|
||||
await message.answer("❌ Этот подарок уже был активирован другим пользователем.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
|
||||
existing_referral = await get_referral_by_referred_id(message.chat.id, session)
|
||||
if not existing_referral:
|
||||
await add_referral(message.chat.id, gift_info["sender_tg_id"], session)
|
||||
|
||||
user_exists = await check_user_exists(message.chat.id)
|
||||
if not user_exists:
|
||||
from_user = message.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
|
||||
await session.execute("UPDATE users SET trial = 1 WHERE tg_id = $1", message.chat.id)
|
||||
|
||||
await create_key(
|
||||
message.chat.id,
|
||||
gift_info["expiry_time"].replace(tzinfo=None),
|
||||
state,
|
||||
session,
|
||||
message,
|
||||
)
|
||||
await session.execute(
|
||||
"UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2",
|
||||
message.chat.id,
|
||||
gift_id,
|
||||
)
|
||||
await message.answer(
|
||||
f"🎉 Ваш подарок на {gift_info['selected_months']} "
|
||||
f"{'месяц' if gift_info['selected_months'] == 1 else 'месяца' if gift_info['selected_months'] in [2, 3, 4] else 'месяцев'} активирован!"
|
||||
)
|
||||
return
|
||||
|
||||
if "referral_" in text:
|
||||
try:
|
||||
referrer_tg_id = int(text.split("referral_")[1])
|
||||
user_exists_now = await check_user_exists(message.chat.id)
|
||||
if user_exists_now:
|
||||
await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
if referrer_tg_id == message.chat.id:
|
||||
await message.answer("❌ Вы не можете быть рефералом самого себя.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
existing_referral = await get_referral_by_referred_id(message.chat.id, session)
|
||||
if existing_referral:
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
|
||||
await add_referral(message.chat.id, referrer_tg_id, session)
|
||||
from_user = message.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
await message.answer(REFERRAL_SUCCESS_MSG.format(referrer_tg_id=referrer_tg_id))
|
||||
try:
|
||||
await bot.send_message(
|
||||
referrer_tg_id,
|
||||
NEW_REFERRAL_NOTIFICATION.format(referred_id=message.chat.id),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить уведомление пригласившему ({referrer_tg_id}): {e}")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
logger.info("Пользователь зашел без реферальной ссылки, подарка или купона.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке текста {message.text} — {e}", exc_info=True)
|
||||
await message.answer("❌ Произошла ошибка. Попробуйте позже.")
|
||||
return
|
||||
|
||||
final_exists = await check_user_exists(message.chat.id)
|
||||
if final_exists:
|
||||
if SHOW_START_MENU_ONCE:
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
else:
|
||||
return await show_start_menu(message, admin, session)
|
||||
else:
|
||||
from_user = message.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
return await show_start_menu(message, admin, session)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "check_subscription")
|
||||
async def check_subscription_callback(callback_query: CallbackQuery, state: FSMContext, session: Any, admin: bool):
|
||||
user_id = callback_query.from_user.id
|
||||
@@ -285,6 +136,117 @@ async def check_subscription_callback(callback_query: CallbackQuery, state: FSMC
|
||||
await callback_query.answer(SUBSCRIPTION_CHECK_ERROR_MSG, show_alert=True)
|
||||
|
||||
|
||||
async def process_start_logic(
|
||||
message: Message, state: FSMContext, session: Any, admin: bool, text_to_process: str = None
|
||||
):
|
||||
text = text_to_process or message.text or message.caption
|
||||
|
||||
if not text:
|
||||
logger.info(f"[StartLogic] Текста нет — вызываю стартовое меню для {message.chat.id}")
|
||||
await show_start_menu(message, admin, session)
|
||||
return
|
||||
|
||||
if text.startswith("/start "):
|
||||
parts = text.split(maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
text = parts[1]
|
||||
|
||||
try:
|
||||
gift_detected = False
|
||||
text_parts = text.split("-")
|
||||
|
||||
for part in text_parts:
|
||||
if "coupons" in part:
|
||||
logger.info(f"Обнаружена ссылка на купон: {part}")
|
||||
coupon_code = part.split("coupons")[1].strip("_")
|
||||
await activate_coupon(message, state, session, coupon_code=coupon_code, admin=admin)
|
||||
continue
|
||||
|
||||
if "gift" in part:
|
||||
parts = part.split("gift")[1].split("_")
|
||||
if len(parts) < 2:
|
||||
await message.answer("❌ Неверный формат ссылки на подарок.")
|
||||
return await process_callback_view_profile(message, state, admin)
|
||||
gift_id = parts[0]
|
||||
await handle_gift_link(gift_id, message, state, session)
|
||||
gift_detected = True
|
||||
break
|
||||
|
||||
if "referral" in part:
|
||||
referrer_tg_id = part.split("referral")[1].strip("_")
|
||||
try:
|
||||
referrer_tg_id = int(referrer_tg_id)
|
||||
await handle_referral_link(referrer_tg_id, message, state, session)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
continue
|
||||
|
||||
if "utm" in part:
|
||||
utm_code = part
|
||||
logger.info(f"Обнаружена ссылка на UTM: {utm_code}")
|
||||
await handle_utm_link(utm_code, message, state, session)
|
||||
continue
|
||||
|
||||
if gift_detected:
|
||||
return
|
||||
|
||||
user_exists = await check_user_exists(message.chat.id)
|
||||
if not user_exists:
|
||||
from_user = message.from_user
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
)
|
||||
|
||||
trial_status = await get_trial(message.chat.id, session)
|
||||
if trial_status > 0:
|
||||
await process_callback_view_profile(message, state, admin)
|
||||
else:
|
||||
await show_start_menu(message, admin, session)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке текста {message.text} — {e}", exc_info=True)
|
||||
await message.answer("❌ Произошла ошибка. Попробуйте позже.")
|
||||
|
||||
|
||||
async def handle_utm_link(utm_code, message, state, session):
|
||||
|
||||
exists = await session.fetchval(
|
||||
"SELECT 1 FROM tracking_sources WHERE code = $1", utm_code
|
||||
)
|
||||
if exists:
|
||||
current_code = await session.fetchval(
|
||||
"SELECT source_code FROM users WHERE tg_id = $1", message.chat.id
|
||||
)
|
||||
if current_code is None:
|
||||
user_exists = await check_user_exists(message.chat.id)
|
||||
from_user = message.from_user
|
||||
if not user_exists:
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
username=from_user.username,
|
||||
first_name=from_user.first_name,
|
||||
last_name=from_user.last_name,
|
||||
language_code=from_user.language_code,
|
||||
is_bot=from_user.is_bot,
|
||||
session=session,
|
||||
source_code=utm_code,
|
||||
)
|
||||
else:
|
||||
await session.execute(
|
||||
"UPDATE users SET source_code = $1 WHERE tg_id = $2",
|
||||
utm_code, message.chat.id
|
||||
)
|
||||
logger.info(f"[UTM] Привязана {utm_code} к пользователю {message.chat.id}")
|
||||
else:
|
||||
await message.answer("❌ UTM ссылка не найдена.")
|
||||
|
||||
|
||||
async def show_start_menu(message: Message, admin: bool, session: Any):
|
||||
"""Функция для отображения стандартного меню через редактирование сообщения.
|
||||
Если редактирование не удалось, отправляем новое сообщение."""
|
||||
|
||||
+9
-24
@@ -242,29 +242,14 @@ async def is_full_remnawave_cluster(cluster_id: str, session) -> bool:
|
||||
return server and server["panel_type"].lower() == "remnawave"
|
||||
|
||||
|
||||
def format_months(months: int) -> str:
|
||||
"""Форматирует количество месяцев с правильным склонением"""
|
||||
if months <= 0:
|
||||
return "0 месяцев"
|
||||
return f"{months} {get_plural_form(months, 'месяц', 'месяца', 'месяцев')}"
|
||||
def sanitize_key_name(key_name: str) -> str:
|
||||
"""
|
||||
Очищает название ключа, оставляя только допустимые символы.
|
||||
|
||||
Args:
|
||||
key_name (str): Исходное название ключа.
|
||||
|
||||
def format_days(days: int) -> str:
|
||||
"""Форматирует количество дней с правильным склонением"""
|
||||
if days <= 0:
|
||||
return "0 дней"
|
||||
return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}"
|
||||
|
||||
|
||||
def format_hours(hours: int) -> str:
|
||||
"""Форматирует количество часов с правильным склонением"""
|
||||
if hours <= 0:
|
||||
return "0 часов"
|
||||
return f"{hours} {get_plural_form(hours, 'час', 'часа', 'часов')}"
|
||||
|
||||
|
||||
def format_minutes(minutes: int) -> str:
|
||||
"""Форматирует количество минут с правильным склонением"""
|
||||
if minutes <= 0:
|
||||
return "0 минут"
|
||||
return f"{minutes} {get_plural_form(minutes, 'минута', 'минуты', 'минут')}"
|
||||
Returns:
|
||||
str: Очищенное название ключа в нижнем регистре.
|
||||
"""
|
||||
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
|
||||
Reference in New Issue
Block a user