ruff formatting
This commit is contained in:
+20
-3
@@ -2,13 +2,17 @@ import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
from rich.console import Console
|
||||
from rich.prompt import Confirm, Prompt
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from config import BOT_SERVICE
|
||||
|
||||
|
||||
try:
|
||||
sys.stdin.reconfigure(encoding="utf-8")
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
@@ -27,10 +31,12 @@ SERVICE_NAME = BOT_SERVICE
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def is_service_exists(service_name):
|
||||
result = subprocess.run(["systemctl", "list-unit-files", service_name], capture_output=True, text=True)
|
||||
return service_name in result.stdout
|
||||
|
||||
|
||||
def print_logo():
|
||||
logo = Text(
|
||||
"""
|
||||
@@ -45,6 +51,7 @@ def print_logo():
|
||||
)
|
||||
console.print(logo)
|
||||
|
||||
|
||||
def backup_project():
|
||||
console.print("[yellow]📦 Создаётся резервная копия проекта...[/yellow]")
|
||||
with console.status("[bold cyan]Копирование файлов...[/bold cyan]"):
|
||||
@@ -52,11 +59,13 @@ def backup_project():
|
||||
subprocess.run(["cp", "-r", PROJECT_DIR, BACK_DIR])
|
||||
console.print(f"[green]✅ Бэкап сохранён в: {BACK_DIR}[/green]")
|
||||
|
||||
|
||||
def install_rsync_if_needed():
|
||||
if subprocess.run(["which", "rsync"], capture_output=True).returncode != 0:
|
||||
console.print("[blue]📦 Установка rsync...[/blue]")
|
||||
os.system("sudo apt update && sudo apt install -y rsync")
|
||||
|
||||
|
||||
def clean_project_dir_safe():
|
||||
console.print("[yellow]🧹 Очистка проекта перед обновлением (кроме config и кнопок)...[/yellow]")
|
||||
preserved_paths = {
|
||||
@@ -92,6 +101,7 @@ def install_git_if_needed():
|
||||
console.print("[blue]Установка Git...[/blue]")
|
||||
os.system("sudo apt update && sudo apt install -y git")
|
||||
|
||||
|
||||
def install_dependencies():
|
||||
console.print("[blue]🔧 Установка зависимостей...[/blue]")
|
||||
with console.status("[bold green]Устанавливаются зависимости...[/bold green]"):
|
||||
@@ -106,6 +116,7 @@ def install_dependencies():
|
||||
except subprocess.CalledProcessError:
|
||||
console.print("[red]❌ Ошибка при установке зависимостей.[/red]")
|
||||
|
||||
|
||||
def restart_service():
|
||||
if is_service_exists(SERVICE_NAME):
|
||||
console.print("[blue]🚀 Перезапуск службы...[/blue]")
|
||||
@@ -114,6 +125,7 @@ def restart_service():
|
||||
else:
|
||||
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
|
||||
|
||||
|
||||
def get_local_version():
|
||||
path = os.path.join(PROJECT_DIR, "bot.py")
|
||||
if not os.path.isfile(path):
|
||||
@@ -125,6 +137,7 @@ def get_local_version():
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def get_remote_version(branch="main"):
|
||||
try:
|
||||
url = f"https://raw.githubusercontent.com/Vladless/Solo_bot/{branch}/bot.py"
|
||||
@@ -138,6 +151,7 @@ def get_remote_version(branch="main"):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def update_from_beta():
|
||||
local_version = get_local_version()
|
||||
remote_version = get_remote_version(branch="dev")
|
||||
@@ -173,6 +187,7 @@ def update_from_beta():
|
||||
restart_service()
|
||||
console.print("[green]✅ Обновление с ветки dev завершено.[/green]")
|
||||
|
||||
|
||||
def update_from_release():
|
||||
if not Confirm.ask("[yellow]🔁 Подтвердите обновление Solobot до одного из последних релизов[/yellow]"):
|
||||
return
|
||||
@@ -194,8 +209,7 @@ def update_from_release():
|
||||
console.print(f"[cyan]{idx}.[/cyan] {tag}")
|
||||
|
||||
selected = Prompt.ask(
|
||||
"[bold blue]Выберите номер релиза[/bold blue]",
|
||||
choices=[str(i) for i in range(1, len(tag_choices) + 1)]
|
||||
"[bold blue]Выберите номер релиза[/bold blue]", choices=[str(i) for i in range(1, len(tag_choices) + 1)]
|
||||
)
|
||||
tag_name = tag_choices[int(selected) - 1]
|
||||
|
||||
@@ -236,8 +250,9 @@ def show_update_menu():
|
||||
elif choice == "2":
|
||||
update_from_release()
|
||||
|
||||
|
||||
def show_menu():
|
||||
table = Table(title=f"Solobot CLI v0.1.4", title_style="bold magenta", header_style="bold blue")
|
||||
table = Table(title="Solobot CLI v0.1.4", title_style="bold magenta", header_style="bold blue")
|
||||
table.add_column("№", justify="center", style="cyan", no_wrap=True)
|
||||
table.add_column("Операция", style="white")
|
||||
table.add_row("1", "Запустить бота (systemd)")
|
||||
@@ -250,6 +265,7 @@ def show_menu():
|
||||
table.add_row("8", "Выход")
|
||||
console.print(table)
|
||||
|
||||
|
||||
def main():
|
||||
os.chdir(PROJECT_DIR)
|
||||
print_logo()
|
||||
@@ -295,5 +311,6 @@ def main():
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[bold red]⏹ Прерывание. Выход из CLI.[/bold red]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+27
-11
@@ -244,7 +244,13 @@ async def add_user(
|
||||
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, source_code
|
||||
tg_id,
|
||||
username,
|
||||
first_name,
|
||||
last_name,
|
||||
language_code,
|
||||
is_bot,
|
||||
source_code,
|
||||
)
|
||||
logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})")
|
||||
except Exception as e:
|
||||
@@ -1361,7 +1367,9 @@ async def get_all_keys(session: Any = None):
|
||||
await conn.close()
|
||||
|
||||
|
||||
async def check_notifications_bulk(notification_type: str, hours: int, session: Any, tg_ids: list[int] = None, emails: list[str] = None) -> list[dict]:
|
||||
async def check_notifications_bulk(
|
||||
notification_type: str, hours: int, session: Any, tg_ids: list[int] = None, emails: list[str] = None
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Проверяет, какие пользователи могут получить уведомление указанного типа, и возвращает их данные.
|
||||
"""
|
||||
@@ -1380,25 +1388,25 @@ async def check_notifications_bulk(notification_type: str, hours: int, session:
|
||||
WHERE (n.last_notification_time IS NULL OR NOW() - n.last_notification_time > ($2 * INTERVAL '1 hour'))
|
||||
"""
|
||||
params = [notification_type, hours]
|
||||
|
||||
|
||||
if tg_ids is not None:
|
||||
query += " AND u.tg_id = ANY($3)"
|
||||
params.append(tg_ids)
|
||||
if emails is not None:
|
||||
query += " AND k.email = ANY($" + str(len(params) + 1) + ")"
|
||||
params.append(emails)
|
||||
|
||||
if notification_type == 'inactive_trial':
|
||||
|
||||
if notification_type == "inactive_trial":
|
||||
query += """
|
||||
AND u.trial IN (0, -1)
|
||||
AND u.tg_id NOT IN (SELECT tg_id FROM blocked_users)
|
||||
AND u.tg_id NOT IN (SELECT DISTINCT tg_id FROM keys)
|
||||
"""
|
||||
|
||||
|
||||
query += """
|
||||
GROUP BY u.tg_id, k.email, u.username, u.first_name, u.last_name
|
||||
"""
|
||||
|
||||
|
||||
users = await session.fetch(query, *params)
|
||||
logger.info(f"Найдено {len(users)} пользователей, готовых к уведомлению типа {notification_type}")
|
||||
return [
|
||||
@@ -1408,7 +1416,9 @@ async def check_notifications_bulk(notification_type: str, hours: int, session:
|
||||
"username": user["username"],
|
||||
"first_name": user["first_name"],
|
||||
"last_name": user["last_name"],
|
||||
"last_notification_time": int(user["last_notification_time"]) if user["last_notification_time"] else None,
|
||||
"last_notification_time": int(user["last_notification_time"])
|
||||
if user["last_notification_time"]
|
||||
else None,
|
||||
}
|
||||
for user in users
|
||||
]
|
||||
@@ -1423,7 +1433,10 @@ async def create_tracking_source(name: str, code: str, type_: str, created_by: i
|
||||
INSERT INTO tracking_sources (name, code, type, created_by)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
""",
|
||||
name, code, type_, created_by
|
||||
name,
|
||||
code,
|
||||
type_,
|
||||
created_by,
|
||||
)
|
||||
|
||||
|
||||
@@ -1446,7 +1459,8 @@ async def get_all_tracking_sources(session) -> list[dict]:
|
||||
|
||||
|
||||
async def get_tracking_source_stats(code: str, session) -> dict:
|
||||
result = await session.fetchrow("""
|
||||
result = await session.fetchrow(
|
||||
"""
|
||||
SELECT
|
||||
ts.name,
|
||||
ts.code,
|
||||
@@ -1463,5 +1477,7 @@ async def get_tracking_source_stats(code: str, session) -> dict:
|
||||
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)
|
||||
""",
|
||||
code,
|
||||
)
|
||||
return dict(result) if result else {}
|
||||
|
||||
@@ -12,8 +12,8 @@ from .notifications import router as notifications_router
|
||||
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
|
||||
from .start import router as start_router
|
||||
|
||||
|
||||
router = Router(name="handlers_main_router")
|
||||
@@ -30,5 +30,5 @@ router.include_routers(
|
||||
keys_router,
|
||||
instructions_router,
|
||||
admin_router,
|
||||
refferal_router
|
||||
refferal_router,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ __all__ = ("router",)
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from .ads import router as ads_router
|
||||
from .backups import router as backups_router
|
||||
from .bans import router as bans_router
|
||||
from .clusters import router as clusters_router
|
||||
@@ -13,7 +14,6 @@ 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")
|
||||
@@ -30,5 +30,5 @@ router.include_routers(
|
||||
coupons_router,
|
||||
restart_router,
|
||||
bans_router,
|
||||
ads_router
|
||||
ads_router,
|
||||
)
|
||||
|
||||
@@ -16,13 +16,14 @@ from logger import logger
|
||||
from ..panel.keyboard import AdminPanelCallback
|
||||
from .keyboard import (
|
||||
AdminAdsCallback,
|
||||
build_ads_delete_confirm_kb,
|
||||
build_ads_kb,
|
||||
build_ads_list_kb,
|
||||
build_ads_stats_kb,
|
||||
build_ads_delete_confirm_kb,
|
||||
build_cancel_input_kb
|
||||
build_cancel_input_kb,
|
||||
)
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -33,33 +34,27 @@ class AdminAdsState(StatesGroup):
|
||||
|
||||
@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()
|
||||
)
|
||||
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> новой ссылки:",
|
||||
reply_markup=build_cancel_input_kb()
|
||||
"📝 Введите <b>название</b> новой ссылки:", reply_markup=build_cancel_input_kb()
|
||||
)
|
||||
|
||||
|
||||
|
||||
@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>.",
|
||||
reply_markup=build_cancel_input_kb()
|
||||
f"🔗 Введите <b>код ссылки</b> для: <code>{name}</code>.", reply_markup=build_cancel_input_kb()
|
||||
)
|
||||
|
||||
|
||||
|
||||
@router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter())
|
||||
async def handle_ads_code_input(message: Message, state: FSMContext, session):
|
||||
code = message.text.strip()
|
||||
@@ -69,11 +64,7 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session):
|
||||
|
||||
try:
|
||||
await create_tracking_source(
|
||||
name=name,
|
||||
code=code_with_prefix,
|
||||
type_="utm",
|
||||
created_by=message.from_user.id,
|
||||
session=session
|
||||
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:
|
||||
@@ -81,7 +72,7 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session):
|
||||
return
|
||||
msg = format_ads_stats(stats, USERNAME_BOT)
|
||||
await message.answer(
|
||||
text=msg,
|
||||
text=msg,
|
||||
reply_markup=build_ads_stats_kb(code_with_prefix),
|
||||
)
|
||||
|
||||
@@ -92,13 +83,14 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session):
|
||||
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)
|
||||
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("Произошла ошибка при получении списка.")
|
||||
@@ -113,7 +105,9 @@ async def handle_ads_view(callback_query: CallbackQuery, callback_data: AdminAds
|
||||
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)
|
||||
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("❌ Ошибка при получении статистики.")
|
||||
@@ -132,15 +126,9 @@ async def handle_ads_delete_confirm(callback_query: CallbackQuery, callback_data
|
||||
async def handle_ads_delete(callback_query: CallbackQuery, callback_data: AdminAdsCallback, session):
|
||||
code = callback_data.code
|
||||
try:
|
||||
await session.execute(
|
||||
"UPDATE users SET source_code = NULL WHERE source_code = $1",
|
||||
code
|
||||
)
|
||||
await session.execute("UPDATE users SET source_code = NULL WHERE source_code = $1", code)
|
||||
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()
|
||||
)
|
||||
await callback_query.message.edit_text(f"🗑️ Ссылка <code>{code}</code> удалена.", reply_markup=build_ads_kb())
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении метки {code}: {e}", exc_info=True)
|
||||
await callback_query.message.edit_text("❌ Не удалось удалить ссылку.")
|
||||
@@ -152,14 +140,11 @@ def format_ads_stats(stats: dict, username_bot: str) -> str:
|
||||
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>."
|
||||
)
|
||||
|
||||
@@ -167,7 +152,4 @@ def format_ads_stats(stats: dict, username_bot: str) -> str:
|
||||
@router.callback_query(AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter())
|
||||
async def handle_ads_cancel_input(callback_query: CallbackQuery, state: FSMContext):
|
||||
await state.clear()
|
||||
await callback_query.message.edit_text(
|
||||
text="📊 <b>Аналитика рекламы:</b>",
|
||||
reply_markup=build_ads_kb()
|
||||
)
|
||||
await callback_query.message.edit_text(text="📊 <b>Аналитика рекламы:</b>", reply_markup=build_ads_kb())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from ..panel.keyboard import build_admin_back_btn
|
||||
|
||||
@@ -9,6 +9,7 @@ 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())
|
||||
@@ -60,20 +61,17 @@ 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()
|
||||
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()
|
||||
|
||||
|
||||
def build_cancel_input_kb() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(
|
||||
text="❌ Отмена",
|
||||
callback_data=AdminAdsCallback(action="cancel_input", code="none").pack()
|
||||
)
|
||||
builder.button(text="❌ Отмена", callback_data=AdminAdsCallback(action="cancel_input", code="none").pack())
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
@@ -4,7 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from handlers.buttons import BACK
|
||||
|
||||
from ..panel.keyboard import build_admin_back_btn, AdminPanelCallback
|
||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
||||
from ..servers.keyboard import AdminServerCallback
|
||||
|
||||
|
||||
@@ -96,12 +96,7 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
|
||||
callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(),
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🔙 Назад",
|
||||
callback_data=AdminPanelCallback(action="clusters").pack()
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack()))
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ def build_panel_kb() -> InlineKeyboardMarkup:
|
||||
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()),
|
||||
)
|
||||
|
||||
@@ -23,11 +23,7 @@ def build_sender_kb() -> InlineKeyboardMarkup:
|
||||
text="📍 Не использовавшие триал", callback_data=AdminSenderCallback(type="untrial").pack()
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="🔥 Горячие лиды", callback_data=AdminSenderCallback(type="hotleads").pack()
|
||||
)
|
||||
)
|
||||
builder.row(InlineKeyboardButton(text="🔥 Горячие лиды", callback_data=AdminSenderCallback(type="hotleads").pack()))
|
||||
builder.row(
|
||||
InlineKeyboardButton(text="📢 Кластер", callback_data=AdminSenderCallback(type="cluster-select").pack())
|
||||
)
|
||||
|
||||
@@ -18,30 +18,19 @@ def build_manage_server_kb(server_name: str, cluster_name: str, enabled: bool) -
|
||||
toggle_text = "🔴 Отключить" if enabled else "🟢 Включить"
|
||||
toggle_action = "disable" if enabled else "enable"
|
||||
|
||||
builder.button(
|
||||
text=toggle_text,
|
||||
callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack()
|
||||
)
|
||||
builder.button(text=toggle_text, callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack())
|
||||
|
||||
builder.button(
|
||||
text="📈 Задать лимит",
|
||||
callback_data=AdminServerCallback(action="set_limit", data=server_name).pack()
|
||||
text="📈 Задать лимит", callback_data=AdminServerCallback(action="set_limit", data=server_name).pack()
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="🗑️ Удалить",
|
||||
callback_data=AdminServerCallback(action="delete", data=server_name).pack()
|
||||
)
|
||||
builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack())
|
||||
|
||||
builder.button(
|
||||
text="✏️ Сменить название",
|
||||
callback_data=AdminServerCallback(action="rename", data=server_name).pack()
|
||||
text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack()
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text=BACK,
|
||||
callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack()
|
||||
)
|
||||
builder.button(text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack())
|
||||
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -9,12 +9,14 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from database import get_servers
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.buttons import BACK
|
||||
|
||||
from ..panel.keyboard import build_admin_back_kb
|
||||
from .keyboard import (
|
||||
AdminServerCallback,
|
||||
build_manage_server_kb,
|
||||
)
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -178,20 +180,14 @@ async def process_callback_delete_server(
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter()
|
||||
)
|
||||
@router.callback_query(AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter())
|
||||
async def toggle_server_enabled(callback_query: CallbackQuery, callback_data: AdminServerCallback, session: Any):
|
||||
server_name = callback_data.data
|
||||
action = callback_data.action
|
||||
|
||||
new_status = action == "enable"
|
||||
|
||||
await session.execute(
|
||||
"UPDATE servers SET enabled = $1 WHERE server_name = $2",
|
||||
new_status,
|
||||
server_name
|
||||
)
|
||||
await session.execute("UPDATE servers SET enabled = $1 WHERE server_name = $2", new_status, server_name)
|
||||
|
||||
servers = await get_servers(include_enabled=True)
|
||||
|
||||
@@ -241,10 +237,7 @@ async def save_server_limit(message: types.Message, state: FSMContext, session:
|
||||
server_name = data["server_name"]
|
||||
|
||||
new_value = limit if limit > 0 else None
|
||||
await session.execute(
|
||||
"UPDATE servers SET max_keys = $1 WHERE server_name = $2",
|
||||
new_value, server_name
|
||||
)
|
||||
await session.execute("UPDATE servers SET max_keys = $1 WHERE server_name = $2", new_value, server_name)
|
||||
|
||||
servers = await get_servers(include_enabled=True)
|
||||
cluster_name, server = next(
|
||||
@@ -268,8 +261,7 @@ async def save_server_limit(message: types.Message, state: FSMContext, session:
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
text,
|
||||
reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True))
|
||||
text, reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True))
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import uuid
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
@@ -20,12 +20,12 @@ from database import (
|
||||
delete_user_data,
|
||||
get_balance,
|
||||
get_client_id_by_email,
|
||||
get_key_details,
|
||||
get_servers,
|
||||
set_user_balance,
|
||||
update_balance,
|
||||
update_key_expiry,
|
||||
update_trial,
|
||||
get_key_details,
|
||||
set_user_balance
|
||||
)
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.keys.key_utils import (
|
||||
@@ -628,12 +628,8 @@ async def process_user_search(
|
||||
created_at = user_data["created_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
|
||||
updated_at = user_data["updated_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
|
||||
|
||||
referral_count = await session.fetchval(
|
||||
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
|
||||
)
|
||||
key_records = await session.fetch(
|
||||
"SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id
|
||||
)
|
||||
referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
|
||||
key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
|
||||
|
||||
text = (
|
||||
f"<b>📊 Информация о пользователе</b>"
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from handlers.texts import CAPTCHA_PROMPT_MSG, CAPTCHA_EMOJIS
|
||||
from handlers.texts import CAPTCHA_EMOJIS, CAPTCHA_PROMPT_MSG
|
||||
from logger import logger
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
+1
-1
@@ -14,8 +14,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from config import ADMIN_ID
|
||||
from database import (
|
||||
add_user,
|
||||
check_user_exists,
|
||||
check_coupon_usage,
|
||||
check_user_exists,
|
||||
create_coupon_usage,
|
||||
get_coupon_by_code,
|
||||
get_keys,
|
||||
|
||||
@@ -98,7 +98,6 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
|
||||
added_days = max(leftover / (1000 * 86400), 0.01)
|
||||
total_gb = int((added_days / 30) * TOTAL_GB * 1024**3)
|
||||
|
||||
|
||||
await renew_key_in_cluster(
|
||||
cluster_id=cluster_id,
|
||||
email=email,
|
||||
|
||||
@@ -24,8 +24,8 @@ from config import (
|
||||
)
|
||||
from database import (
|
||||
add_user,
|
||||
check_user_exists,
|
||||
check_server_name_by_cluster,
|
||||
check_user_exists,
|
||||
get_key_details,
|
||||
get_trial,
|
||||
update_balance,
|
||||
@@ -247,7 +247,6 @@ async def finalize_key_creation(
|
||||
)
|
||||
logger.info(f"[User] Новый пользователь {tg_id} добавлен")
|
||||
|
||||
|
||||
expiry_time = expiry_time.astimezone(moscow_tz)
|
||||
|
||||
if old_key_name:
|
||||
@@ -459,7 +458,7 @@ async def check_server_availability(server_info: dict, session: Any) -> bool:
|
||||
panel_type = server_info.get("panel_type", "3x-ui").lower()
|
||||
enabled = server_info.get("enabled", True)
|
||||
max_keys = server_info.get("max_keys")
|
||||
|
||||
|
||||
if not enabled:
|
||||
logger.info(f"[Ping] Сервер {server_name} выключен (enabled = FALSE).")
|
||||
return False
|
||||
@@ -494,4 +493,4 @@ async def check_server_availability(server_info: dict, session: Any) -> bool:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"[Ping] Ошибка при проверке сервера {server_name}: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -180,11 +180,7 @@ async def create_key(
|
||||
Также отвечает за первичное подключение пользователя.
|
||||
"""
|
||||
if not await check_user_exists(tg_id):
|
||||
from_user = (
|
||||
message_or_query.from_user
|
||||
if isinstance(message_or_query, (CallbackQuery, Message))
|
||||
else None
|
||||
)
|
||||
from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None
|
||||
if from_user:
|
||||
await add_user(
|
||||
tg_id=from_user.id,
|
||||
|
||||
@@ -76,7 +76,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any
|
||||
)
|
||||
)
|
||||
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys"))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record['email']}"))
|
||||
|
||||
balance = await get_balance(tg_id)
|
||||
|
||||
|
||||
+10
-11
@@ -5,6 +5,7 @@ from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
from bot import bot
|
||||
from config import (
|
||||
DATABASE_URL,
|
||||
LIMIT_IP,
|
||||
@@ -15,7 +16,7 @@ from config import (
|
||||
TOTAL_GB,
|
||||
)
|
||||
from database import delete_notification, get_servers, store_key
|
||||
from handlers.utils import get_least_loaded_cluster, check_server_key_limit
|
||||
from handlers.utils import check_server_key_limit, get_least_loaded_cluster
|
||||
from logger import logger
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
from panels.three_xui import (
|
||||
@@ -24,12 +25,10 @@ from panels.three_xui import (
|
||||
delete_client,
|
||||
extend_client_key,
|
||||
get_client_traffic,
|
||||
get_xui_instance,
|
||||
toggle_client,
|
||||
get_xui_instance
|
||||
)
|
||||
|
||||
from bot import bot
|
||||
|
||||
|
||||
async def create_key_on_cluster(
|
||||
cluster_id: str,
|
||||
@@ -66,14 +65,14 @@ async def create_key_on_cluster(
|
||||
async with asyncpg.create_pool(DATABASE_URL) as pool:
|
||||
async with pool.acquire() as conn:
|
||||
remnawave_servers = [
|
||||
s for s in enabled_servers
|
||||
if s.get("panel_type", "3x-ui").lower() == "remnawave"
|
||||
and await check_server_key_limit(s, conn)
|
||||
s
|
||||
for s in enabled_servers
|
||||
if s.get("panel_type", "3x-ui").lower() == "remnawave" and await check_server_key_limit(s, conn)
|
||||
]
|
||||
xui_servers = [
|
||||
s for s in enabled_servers
|
||||
if s.get("panel_type", "3x-ui").lower() == "3x-ui"
|
||||
and await check_server_key_limit(s, conn)
|
||||
s
|
||||
for s in enabled_servers
|
||||
if s.get("panel_type", "3x-ui").lower() == "3x-ui" and await check_server_key_limit(s, conn)
|
||||
]
|
||||
|
||||
if not remnawave_servers and not xui_servers:
|
||||
@@ -789,4 +788,4 @@ async def reset_traffic_in_cluster(cluster_id: str, email: str) -> None:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
@@ -17,7 +17,6 @@ from config import (
|
||||
CONNECT_PHONE_BUTTON,
|
||||
ENABLE_DELETE_KEY_BUTTON,
|
||||
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
|
||||
PUBLIC_LINK,
|
||||
QRCODE,
|
||||
TOGGLE_CLIENT,
|
||||
USE_COUNTRY_SELECTION,
|
||||
@@ -48,7 +47,14 @@ from handlers.texts import (
|
||||
NO_SUBSCRIPTIONS_MSG,
|
||||
key_message,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message, handle_error, is_full_remnawave_cluster, format_days, format_hours, format_minutes
|
||||
from handlers.utils import (
|
||||
edit_or_send_message,
|
||||
format_days,
|
||||
format_hours,
|
||||
format_minutes,
|
||||
handle_error,
|
||||
is_full_remnawave_cluster,
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -221,7 +227,9 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
|
||||
days = total_seconds // 86400
|
||||
hours = (total_seconds % 86400) // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
days_left_message = f"Осталось: <b>{format_days(days)}</b>, <b>{format_hours(hours)}</b>, <b>{format_minutes(minutes)}</b>"
|
||||
days_left_message = (
|
||||
f"Осталось: <b>{format_days(days)}</b>, <b>{format_hours(hours)}</b>, <b>{format_minutes(minutes)}</b>"
|
||||
)
|
||||
|
||||
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
|
||||
response_message = key_message(
|
||||
|
||||
@@ -135,7 +135,9 @@ def format_time_left(expiry_time_ms: int | None) -> str:
|
||||
return f"{days}D,{hours}H ⏳" if days else f"{hours}H ⏳"
|
||||
|
||||
|
||||
def prepare_headers(user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str) -> dict[str, str]:
|
||||
def prepare_headers(
|
||||
user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str
|
||||
) -> dict[str, str]:
|
||||
if "Happ" in user_agent:
|
||||
encoded_project_name = f"{project_name}"
|
||||
announce_str = f"↖️Бот | {subscription_info} | Поддержка↗️"
|
||||
@@ -160,7 +162,7 @@ def prepare_headers(user_agent: str, project_name: str, subscription_info: str,
|
||||
}
|
||||
elif "v2raytun" in user_agent:
|
||||
encoded_project_name = f"{project_name}\n{subscription_info}"
|
||||
announce_str = f"🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️"
|
||||
announce_str = "🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️"
|
||||
return {
|
||||
"Content-Type": "text/plain; charset=utf-8",
|
||||
"Content-Disposition": "inline",
|
||||
|
||||
@@ -22,8 +22,8 @@ from config import (
|
||||
)
|
||||
from database import (
|
||||
add_notification,
|
||||
check_notifications_bulk,
|
||||
check_notification_time,
|
||||
check_notifications_bulk,
|
||||
delete_key,
|
||||
delete_notification,
|
||||
get_all_keys,
|
||||
@@ -48,7 +48,7 @@ from handlers.texts import (
|
||||
from handlers.utils import format_hours, format_minutes
|
||||
from logger import logger
|
||||
|
||||
from .notify_utils import send_notification, send_messages_with_limit
|
||||
from .notify_utils import send_messages_with_limit, send_notification
|
||||
from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic
|
||||
|
||||
|
||||
@@ -331,19 +331,15 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
if hours > 0:
|
||||
if minutes > 0:
|
||||
delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format(
|
||||
email=email,
|
||||
hours_formatted=format_hours(hours),
|
||||
minutes_formatted=format_minutes(minutes)
|
||||
email=email, hours_formatted=format_hours(hours), minutes_formatted=format_minutes(minutes)
|
||||
)
|
||||
else:
|
||||
delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format(
|
||||
email=email,
|
||||
hours_formatted=format_hours(hours)
|
||||
email=email, hours_formatted=format_hours(hours)
|
||||
)
|
||||
else:
|
||||
delay_message = KEY_EXPIRED_DELAY_MINUTES_MSG.format(
|
||||
email=email,
|
||||
minutes_formatted=format_minutes(minutes)
|
||||
email=email, minutes_formatted=format_minutes(minutes)
|
||||
)
|
||||
else:
|
||||
delay_message = KEY_EXPIRED_NO_DELAY_MSG.format(email=email)
|
||||
@@ -431,7 +427,9 @@ async def process_auto_renew_or_notify(
|
||||
if result:
|
||||
logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.")
|
||||
else:
|
||||
logger.warning(f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}.")
|
||||
logger.warning(
|
||||
f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}."
|
||||
)
|
||||
except KeyError as e:
|
||||
logger.error(f"❌ Ошибка форматирования сообщения KEY_RENEWED: отсутствует ключ {e}")
|
||||
except Exception as e:
|
||||
@@ -443,4 +441,4 @@ async def process_auto_renew_or_notify(
|
||||
logger.info(f"📢 Отправлено уведомление об истекающей подписке {email} пользователю {tg_id}.")
|
||||
await add_notification(tg_id, notification_id, session=conn)
|
||||
else:
|
||||
logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.")
|
||||
logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.")
|
||||
|
||||
@@ -16,16 +16,10 @@ async def send_messages_with_limit(bot: Bot, messages: list[dict], messages_per_
|
||||
"""
|
||||
batch_size = messages_per_second
|
||||
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:
|
||||
tasks.append(send_notification(
|
||||
bot,
|
||||
msg["tg_id"],
|
||||
msg.get("photo"),
|
||||
msg["text"],
|
||||
msg.get("keyboard")
|
||||
))
|
||||
tasks.append(send_notification(bot, msg["tg_id"], msg.get("photo"), msg["text"], msg.get("keyboard")))
|
||||
try:
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
except Exception as e:
|
||||
@@ -50,6 +44,7 @@ def rate_limited_send(func):
|
||||
tg_id = kwargs.get("tg_id") or args[1]
|
||||
logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}")
|
||||
return False
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -65,7 +60,7 @@ async def send_notification(
|
||||
"""
|
||||
if image_filename is None:
|
||||
return await _send_text_notification(bot, tg_id, caption, keyboard)
|
||||
|
||||
|
||||
photo_path = os.path.join("img", image_filename)
|
||||
if os.path.isfile(photo_path):
|
||||
return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard)
|
||||
@@ -114,4 +109,4 @@ async def _send_text_notification(
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -20,7 +20,8 @@ from handlers.keys.key_utils import get_user_traffic
|
||||
from handlers.texts import TRIAL_INACTIVE_BONUS_MSG, TRIAL_INACTIVE_FIRST_MSG, ZERO_TRAFFIC_MSG
|
||||
from handlers.utils import format_days
|
||||
from logger import logger
|
||||
from .notify_utils import send_notification, send_messages_with_limit
|
||||
|
||||
from .notify_utils import send_messages_with_limit, send_notification
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -70,8 +71,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
|
||||
await conn.execute("UPDATE users SET trial = -1 WHERE tg_id = $1", tg_id)
|
||||
else:
|
||||
message = TRIAL_INACTIVE_FIRST_MSG.format(
|
||||
display_name=display_name,
|
||||
trial_time_formatted=format_days(TRIAL_TIME)
|
||||
display_name=display_name, trial_time_formatted=format_days(TRIAL_TIME)
|
||||
)
|
||||
|
||||
messages.append({
|
||||
@@ -175,4 +175,4 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обновления notified для пользователя {tg_id}: {e}")
|
||||
|
||||
logger.info("✅ Обработка пользователей с нулевым трафиком завершена.")
|
||||
logger.info("✅ Обработка пользователей с нулевым трафиком завершена.")
|
||||
|
||||
+17
-5
@@ -1,8 +1,10 @@
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
|
||||
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,
|
||||
@@ -12,9 +14,19 @@ from config import (
|
||||
YOOKASSA_ENABLE,
|
||||
YOOMONEY_ENABLE,
|
||||
)
|
||||
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 handlers.buttons import (
|
||||
BALANCE_HISTORY,
|
||||
COUPON,
|
||||
CRYPTOBOT,
|
||||
MAIN_MENU,
|
||||
PAYMENT,
|
||||
ROBOKASSA,
|
||||
STARS,
|
||||
YOOKASSA,
|
||||
YOOMONEY,
|
||||
)
|
||||
from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG
|
||||
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
@@ -131,4 +143,4 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any):
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=None,
|
||||
disable_web_page_preview=False,
|
||||
)
|
||||
)
|
||||
|
||||
+12
-7
@@ -1,15 +1,14 @@
|
||||
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.fsm.context import FSMContext
|
||||
from aiogram.types import (
|
||||
CallbackQuery,
|
||||
InlineKeyboardButton,
|
||||
@@ -20,6 +19,7 @@ from aiogram.types import (
|
||||
)
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot import bot
|
||||
from config import (
|
||||
ADMIN_ID,
|
||||
DATABASE_URL,
|
||||
@@ -28,7 +28,7 @@ from config import (
|
||||
TRIAL_TIME,
|
||||
USERNAME_BOT,
|
||||
)
|
||||
from database import get_referral_stats, get_referral_by_referred_id, add_referral, add_user, check_user_exists
|
||||
from database import add_referral, add_user, check_user_exists, get_referral_by_referred_id, get_referral_stats
|
||||
from handlers.buttons import (
|
||||
BACK,
|
||||
INVITE,
|
||||
@@ -36,17 +36,22 @@ from handlers.buttons import (
|
||||
QR,
|
||||
TOP_FIVE,
|
||||
)
|
||||
from handlers.texts import INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT, REFERRAL_OFFERS, REFERRAL_SUCCESS_MSG, NEW_REFERRAL_NOTIFICATION
|
||||
from handlers.texts import (
|
||||
INVITE_TEXT_NON_INLINE,
|
||||
NEW_REFERRAL_NOTIFICATION,
|
||||
REFERRAL_OFFERS,
|
||||
REFERRAL_SUCCESS_MSG,
|
||||
TOP_REFERRALS_TEXT,
|
||||
)
|
||||
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):
|
||||
@@ -253,4 +258,4 @@ async def handle_referral_link(referral_code: str, message: Message, state: FSMC
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке реферальной ссылки {referral_code}: {e}")
|
||||
await message.answer("❌ Произошла ошибка при обработке реферальной ссылки.")
|
||||
return
|
||||
return
|
||||
|
||||
+6
-14
@@ -24,14 +24,14 @@ from config import (
|
||||
SUPPORT_CHAT_URL,
|
||||
)
|
||||
from database import (
|
||||
get_trial,
|
||||
add_user,
|
||||
check_user_exists,
|
||||
get_trial,
|
||||
)
|
||||
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.payments.gift import handle_gift_link
|
||||
from handlers.profile import process_callback_view_profile
|
||||
from handlers.texts import (
|
||||
NOT_SUBSCRIBED_YET_MSG,
|
||||
@@ -44,8 +44,8 @@ from handlers.texts import (
|
||||
from logger import logger
|
||||
|
||||
from .admin.panel.keyboard import AdminPanelCallback
|
||||
from .utils import edit_or_send_message
|
||||
from .refferal import handle_referral_link
|
||||
from .utils import edit_or_send_message
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -219,14 +219,9 @@ async def process_start_logic(
|
||||
|
||||
|
||||
async def handle_utm_link(utm_code, message, state, session):
|
||||
|
||||
exists = await session.fetchval(
|
||||
"SELECT 1 FROM tracking_sources WHERE code = $1", utm_code
|
||||
)
|
||||
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
|
||||
)
|
||||
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
|
||||
@@ -242,10 +237,7 @@ async def handle_utm_link(utm_code, message, state, session):
|
||||
source_code=utm_code,
|
||||
)
|
||||
else:
|
||||
await session.execute(
|
||||
"UPDATE users SET source_code = $1 WHERE tg_id = $2",
|
||||
utm_code, message.chat.id
|
||||
)
|
||||
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 ссылка не найдена.")
|
||||
|
||||
+7
-4
@@ -11,7 +11,7 @@ import asyncpg
|
||||
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message
|
||||
|
||||
from bot import bot
|
||||
from config import DATABASE_URL, ADMIN_ID
|
||||
from config import ADMIN_ID, DATABASE_URL
|
||||
from database import get_all_keys, get_servers
|
||||
from logger import logger
|
||||
|
||||
@@ -85,8 +85,7 @@ async def check_server_key_limit(server_info: dict, conn) -> bool:
|
||||
if usage_percent >= 0.9:
|
||||
notif_key = f"server_warn_{server_name}"
|
||||
already_sent = await conn.fetchval(
|
||||
"SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = 0 AND notification_type = $1)",
|
||||
notif_key
|
||||
"SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = 0 AND notification_type = $1)", notif_key
|
||||
)
|
||||
if not already_sent:
|
||||
for admin_id in ADMIN_ID:
|
||||
@@ -130,12 +129,14 @@ def get_plural_form(num: int, form1: str, form2: str, form3: str) -> str:
|
||||
return form3
|
||||
return {1: form1, 2: form2, 3: form2, 4: form2}.get(n % 10, form3)
|
||||
|
||||
|
||||
def format_months(months: int) -> str:
|
||||
"""Форматирует количество месяцев с правильным склонением"""
|
||||
if months <= 0:
|
||||
return "0 месяцев"
|
||||
return f"{months} {get_plural_form(months, 'месяц', 'месяца', 'месяцев')}"
|
||||
|
||||
|
||||
def format_days(days: int) -> str:
|
||||
"""
|
||||
Форматирует количество дней с правильным склонением.
|
||||
@@ -144,12 +145,14 @@ def format_days(days: int) -> str:
|
||||
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:
|
||||
@@ -252,4 +255,4 @@ def sanitize_key_name(key_name: str) -> str:
|
||||
Returns:
|
||||
str: Очищенное название ключа в нижнем регистре.
|
||||
"""
|
||||
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
|
||||
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
|
||||
|
||||
+9
-11
@@ -1,18 +1,20 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import py3xui
|
||||
|
||||
from py3xui import AsyncApi
|
||||
import time
|
||||
|
||||
from config import (
|
||||
LIMIT_IP,
|
||||
SUPERNODE,
|
||||
XUI_TOKEN,
|
||||
USE_XUI_TOKEN,
|
||||
ADMIN_PASSWORD,
|
||||
ADMIN_USERNAME,
|
||||
LIMIT_IP,
|
||||
SUPERNODE,
|
||||
USE_XUI_TOKEN,
|
||||
XUI_TOKEN,
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
@@ -36,6 +38,7 @@ class ClientConfig:
|
||||
_xui_instance_cache: dict[str, tuple[AsyncApi, float]] = {}
|
||||
SESSION_TTL = 1800
|
||||
|
||||
|
||||
async def get_xui_instance(api_url: str) -> AsyncApi:
|
||||
key = f"{api_url}|{ADMIN_USERNAME}"
|
||||
current_time = time.time()
|
||||
@@ -46,7 +49,7 @@ async def get_xui_instance(api_url: str) -> AsyncApi:
|
||||
if current_time - last_login < SESSION_TTL:
|
||||
return xui
|
||||
else:
|
||||
logger.info(f"[XUI Cache] Сессия устарела (>30 минут), переподключение...")
|
||||
logger.info("[XUI Cache] Сессия устарела (>30 минут), переподключение...")
|
||||
await xui.login()
|
||||
_xui_instance_cache[key] = (xui, current_time)
|
||||
return xui
|
||||
@@ -65,7 +68,6 @@ async def get_xui_instance(api_url: str) -> AsyncApi:
|
||||
|
||||
async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]:
|
||||
try:
|
||||
|
||||
client = py3xui.Client(
|
||||
id=config.client_id,
|
||||
email=config.email.lower(),
|
||||
@@ -107,7 +109,6 @@ async def extend_client_key(
|
||||
tg_id: int,
|
||||
) -> bool | None:
|
||||
try:
|
||||
|
||||
client = await xui.client.get_by_email(email)
|
||||
if not client or not client.id:
|
||||
logger.warning(f"Клиент с email {email} не найден или не имеет ID.")
|
||||
@@ -158,7 +159,6 @@ async def delete_client(
|
||||
bool: True если удаление успешно, False в противном случае
|
||||
"""
|
||||
try:
|
||||
|
||||
if SUPERNODE:
|
||||
await xui.client.delete(inbound_id, client_id)
|
||||
logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)")
|
||||
@@ -185,7 +185,6 @@ async def delete_client(
|
||||
|
||||
async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, Any]:
|
||||
try:
|
||||
|
||||
traffic_data = await xui.client.get_traffic_by_id(client_id)
|
||||
if not traffic_data:
|
||||
logger.warning(f"Трафик для клиента {client_id} не найден.")
|
||||
@@ -205,7 +204,6 @@ async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str,
|
||||
|
||||
async def toggle_client(xui: py3xui.AsyncApi, inbound_id: int, email: str, client_id: str, enable: bool = True) -> bool:
|
||||
try:
|
||||
|
||||
client = await xui.client.get_by_email(email)
|
||||
if not client:
|
||||
logger.warning(f"Клиент с email {email} и ID {client_id} не найден.")
|
||||
|
||||
Reference in New Issue
Block a user