ruff formatting

This commit is contained in:
Vladless
2025-04-24 23:20:18 +03:00
parent ff498feb7e
commit 22121b9171
30 changed files with 198 additions and 213 deletions
+20 -3
View File
@@ -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
View File
@@ -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 {}
+2 -2
View File
@@ -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 -2
View File
@@ -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,
)
+17 -35
View File
@@ -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())
+9 -11
View File
@@ -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
+2 -7
View File
@@ -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()
-1
View File
@@ -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()),
)
+1 -5
View File
@@ -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())
)
+6 -17
View File
@@ -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()
+6 -14
View File
@@ -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()
+5 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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,
-1
View File
@@ -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,
+3 -4
View File
@@ -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
+1 -5
View File
@@ -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,
+1 -1
View File
@@ -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
View File
@@ -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
+11 -3
View File
@@ -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(
+4 -2
View File
@@ -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}.")
+5 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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} не найден.")