Add antiflood and lint

This commit is contained in:
Zakhar Izmaylov
2025-01-10 14:51:13 +03:00
parent 5c70f8c35f
commit a018a42f2c
26 changed files with 491 additions and 250 deletions
+1 -1
View File
@@ -3,8 +3,8 @@ import subprocess
from datetime import datetime
from aiogram.types import BufferedInputFile
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
from logger import logger
+6 -1
View File
@@ -5,13 +5,14 @@ from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import ErrorEvent
from config import API_TOKEN
from logger import logger
from middlewares.admin import AdminMiddleware
from middlewares.database import DatabaseMiddleware
from middlewares.delete import DeleteMessageMiddleware
from middlewares.logging import LoggingMiddleware
from middlewares.throttling import ThrottlingMiddleware
from middlewares.user import UserMiddleware
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
@@ -29,6 +30,10 @@ dp.callback_query.middleware(UserMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
# Add throttling middleware
dp.message.middleware(ThrottlingMiddleware(limit=1)) # 1 message per second
dp.callback_query.middleware(ThrottlingMiddleware(limit=1))
dp.message.outer_middleware(DeleteMessageMiddleware())
dp.callback_query.outer_middleware(DeleteMessageMiddleware())
+4 -3
View File
@@ -1,6 +1,6 @@
import py3xui
from config import LIMIT_IP
from logger import logger
@@ -44,14 +44,15 @@ async def add_client(
error_message = str(e)
if "Duplicate email" in error_message:
logger.warning(f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}")
logger.warning(
f"Дублированный email: {email}. Пропуск. Сообщение: {error_message}"
)
return {"status": "duplicate", "email": email}
logger.error(f"Ошибка при добавлении клиента {email}: {error_message}")
return {"status": "failed", "error": error_message}
async def extend_client_key(
xui, inbound_id, email: str, new_expiry_time: int, client_id: str, total_gb: int
):
+25 -20
View File
@@ -3,8 +3,8 @@ from datetime import datetime
from typing import Any
import asyncpg
from config import DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
@@ -17,36 +17,34 @@ async def save_temporary_data(session, tg_id: int, state: str, data: dict):
ON CONFLICT (tg_id)
DO UPDATE SET state = $2, data = $3, updated_at = $4
""",
tg_id, state, json.dumps(data), datetime.utcnow()
tg_id,
state,
json.dumps(data),
datetime.utcnow(),
)
async def get_temporary_data(session, tg_id: int) -> dict | None:
"""Извлекает временные данные пользователя."""
result = await session.fetchrow(
"SELECT state, data FROM temporary_data WHERE tg_id = $1",
tg_id
"SELECT state, data FROM temporary_data WHERE tg_id = $1", tg_id
)
if result:
return {
"state": result["state"],
"data": json.loads(result["data"])
}
return {"state": result["state"], "data": json.loads(result["data"])}
return None
async def clear_temporary_data(session, tg_id: int):
await session.execute(
"DELETE FROM temporary_data WHERE tg_id = $1",
tg_id
)
await session.execute("DELETE FROM temporary_data WHERE tg_id = $1", tg_id)
async def add_blocked_user(tg_id: int, conn: asyncpg.Connection):
await conn.execute(
"INSERT INTO blocked_users (tg_id) VALUES ($1) ON CONFLICT (tg_id) DO NOTHING",
tg_id
tg_id,
)
async def init_db(file_path: str = "assets/schema.sql"):
with open(file_path) as file:
sql_content = file.read()
@@ -610,7 +608,9 @@ async def add_referral(referred_tg_id: int, referrer_tg_id: int, session: Any):
try:
if referred_tg_id == referrer_tg_id:
logger.warning(f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку.")
logger.warning(
f"Пользователь {referred_tg_id} попытался использовать свою собственную реферальную ссылку."
)
return
await session.execute(
@@ -1244,11 +1244,11 @@ async def get_servers_from_db():
async def delete_user_data(session: Any, tg_id: int):
try:
await session.execute("DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id)
except Exception as e:
logger.warning(
f"У Вас версия без подарков для {tg_id}: {e}"
await session.execute(
"DELETE FROM gifts WHERE sender_tg_id = $1 OR recipient_tg_id = $1", tg_id
)
except Exception as e:
logger.warning(f"У Вас версия без подарков для {tg_id}: {e}")
await session.execute("DELETE FROM payments WHERE tg_id = $1", tg_id)
await session.execute("DELETE FROM users WHERE tg_id = $1", tg_id)
await session.execute("DELETE FROM connections WHERE tg_id = $1", tg_id)
@@ -1257,7 +1257,12 @@ async def delete_user_data(session: Any, tg_id: int):
async def store_gift_link(
gift_id: str, sender_tg_id: int, selected_months: int, expiry_time: datetime, gift_link: str, session: Any = None
gift_id: str,
sender_tg_id: int,
selected_months: int,
expiry_time: datetime,
gift_link: str,
session: Any = None,
):
"""
Добавляет информацию о подарке в базу данных.
-2
View File
@@ -1,7 +1,5 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+70 -46
View File
@@ -10,10 +10,10 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL
from backup import backup_database
from bot import bot
from config import DATABASE_URL
from database import delete_user_data
from filters.admin import IsAdminFilter
from logger import logger
@@ -37,7 +37,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
async def handle_admin_message(message: types.Message, state: FSMContext):
await state.clear()
BOT_VERSION = "4.0.0-preAlpha(9)"
BOT_VERSION = "4.0.0-preAlpha(9)"
builder = InlineKeyboardBuilder()
builder.row(
@@ -64,17 +64,12 @@ async def handle_admin_message(message: types.Message, state: FSMContext):
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to")
)
builder.row(
InlineKeyboardButton(
text="🤖 Управление Ботом", callback_data="bot_management"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
InlineKeyboardButton(text="🤖 Управление Ботом", callback_data="bot_management")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.answer(
f"🤖 Панель администратора\n\nВерсия бота: <b>{BOT_VERSION}</b>",
reply_markup=builder.as_markup(),
parse_mode="HTML"
reply_markup=builder.as_markup()
)
@@ -87,19 +82,14 @@ async def handle_bot_management(callback_query: types.CallbackQuery):
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
builder.row(
InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user")
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="🚫 Баны", callback_data="ban_user"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="admin"))
await callback_query.message.answer(
"🤖 Управление ботом",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "user_stats", IsAdminFilter())
async def user_stats_menu(callback_query: CallbackQuery, session: Any):
try:
@@ -209,7 +199,9 @@ async def export_users_csv(callback_query: CallbackQuery, session: Any):
)
return
csv_data = "tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n"
csv_data = (
"tg_id,username,first_name,last_name,language_code,is_bot,balance,trial\n"
)
for user in users:
csv_data += f"{user['tg_id']},{user['username']},{user['first_name']},{user['last_name']},{user['language_code']},{user['is_bot']},{user['balance']},{user['trial']}\n"
@@ -285,16 +277,31 @@ async def export_payments_csv(callback_query: CallbackQuery, session: Any):
@router.callback_query(F.data == "send_to", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all"))
builder.row(InlineKeyboardButton(text="📢 Отправить с подпиской", callback_data="send_to_subscribed"))
builder.row(InlineKeyboardButton(text="📢 Отправить без подписки", callback_data="send_to_unsubscribed"))
builder.row(InlineKeyboardButton(text="📢 Рассылка по кластеру", callback_data="send_to_cluster"))
builder.row(
InlineKeyboardButton(text="📢 Отправить всем", callback_data="send_to_all")
)
builder.row(
InlineKeyboardButton(
text="📢 Отправить с подпиской", callback_data="send_to_subscribed"
)
)
builder.row(
InlineKeyboardButton(
text="📢 Отправить без подписки", callback_data="send_to_unsubscribed"
)
)
builder.row(
InlineKeyboardButton(
text="📢 Рассылка по кластеру", callback_data="send_to_cluster"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="admin"))
await callback_query.message.answer(
"✍️ Выберите группу пользователей и введите текст сообщения для рассылки:",
reply_markup=builder.as_markup(),
)
@router.callback_query(F.data == "send_to_all", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="all")
@@ -303,6 +310,7 @@ async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
)
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_subscribed", IsAdminFilter())
async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="subscribed")
@@ -311,6 +319,7 @@ async def handle_send_to_subscribed(callback_query: CallbackQuery, state: FSMCon
)
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_unsubscribed", IsAdminFilter())
async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMContext):
await state.update_data(send_to="unsubscribed")
@@ -319,8 +328,11 @@ async def handle_send_to_unsubscribed(callback_query: CallbackQuery, state: FSMC
)
await state.set_state(UserEditorState.waiting_for_message)
@router.callback_query(F.data == "send_to_cluster", IsAdminFilter())
async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContext, session: Any):
async def handle_send_to_cluster(
callback_query: CallbackQuery, state: FSMContext, session: Any
):
clusters = await session.fetch("SELECT DISTINCT cluster_name FROM servers")
builder = InlineKeyboardBuilder()
@@ -328,7 +340,7 @@ async def handle_send_to_cluster(callback_query: CallbackQuery, state: FSMContex
builder.row(
InlineKeyboardButton(
text=f"🌐 {cluster['cluster_name']}",
callback_data=f"send_cluster|{cluster['cluster_name']}"
callback_data=f"send_cluster|{cluster['cluster_name']}",
)
)
@@ -357,34 +369,43 @@ async def process_message_to_all(
try:
state_data = await state.get_data()
send_to = state_data.get('send_to', 'all')
send_to = state_data.get("send_to", "all")
if send_to == 'all':
if send_to == "all":
tg_ids = await session.fetch("SELECT DISTINCT tg_id FROM connections")
elif send_to == 'subscribed':
tg_ids = await session.fetch("""
elif send_to == "subscribed":
tg_ids = await session.fetch(
"""
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.expiry_time > $1
""", int(datetime.utcnow().timestamp() * 1000))
elif send_to == 'unsubscribed':
tg_ids = await session.fetch("""
""",
int(datetime.utcnow().timestamp() * 1000),
)
elif send_to == "unsubscribed":
tg_ids = await session.fetch(
"""
SELECT c.tg_id
FROM connections c
LEFT JOIN keys k ON c.tg_id = k.tg_id
GROUP BY c.tg_id
HAVING COUNT(k.tg_id) = 0 OR MAX(k.expiry_time) <= $1
""", int(datetime.utcnow().timestamp() * 1000))
elif send_to == 'cluster':
cluster_name = state_data.get('cluster_name')
tg_ids = await session.fetch("""
""",
int(datetime.utcnow().timestamp() * 1000),
)
elif send_to == "cluster":
cluster_name = state_data.get("cluster_name")
tg_ids = await session.fetch(
"""
SELECT DISTINCT c.tg_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
JOIN servers s ON k.server_id = s.cluster_name
WHERE s.cluster_name = $1
""", cluster_name)
""",
cluster_name,
)
total_users = len(tg_ids)
success_count = 0
@@ -503,11 +524,11 @@ async def handle_ban_user(callback_query: types.CallbackQuery):
InlineKeyboardButton(text="📄 Выгрузить в CSV", callback_data="export_to_csv")
)
builder.row(
InlineKeyboardButton(text="🗑️ Удалить из БД", callback_data="delete_banned_users")
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management")
InlineKeyboardButton(
text="🗑️ Удалить из БД", callback_data="delete_banned_users"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="bot_management"))
await callback_query.message.answer(
"🚫 Заблокировавшие бота\n\n"
"Здесь можно просматривать и удалять пользователей, которые забанили вашего бота!",
@@ -523,6 +544,7 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery):
import csv
import io
csv_output = io.StringIO()
writer = csv.writer(csv_output)
writer.writerow(["tg_id", "blocked_at"])
@@ -532,8 +554,7 @@ async def export_banned_users_to_csv(callback_query: types.CallbackQuery):
csv_output.seek(0)
document = BufferedInputFile(
file=csv_output.getvalue().encode("utf-8"),
filename="banned_users.csv"
file=csv_output.getvalue().encode("utf-8"), filename="banned_users.csv"
)
builder = InlineKeyboardBuilder()
@@ -564,16 +585,20 @@ async def delete_banned_users(callback_query: types.CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
try:
blocked_users = await conn.fetch("SELECT tg_id FROM blocked_users")
blocked_ids = [record['tg_id'] for record in blocked_users]
blocked_ids = [record["tg_id"] for record in blocked_users]
if not blocked_ids:
await callback_query.message.answer("📂 Нет заблокировавших пользователей для удаления.")
await callback_query.message.answer(
"📂 Нет заблокировавших пользователей для удаления."
)
return
for tg_id in blocked_ids:
await delete_user_data(conn, tg_id)
await conn.execute("DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids)
await conn.execute(
"DELETE FROM blocked_users WHERE tg_id = ANY($1)", blocked_ids
)
builder = InlineKeyboardBuilder()
builder.row(
@@ -594,4 +619,3 @@ async def delete_banned_users(callback_query: types.CallbackQuery):
)
finally:
await conn.close()
+6 -3
View File
@@ -6,10 +6,10 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from py3xui import AsyncApi
from backup import create_backup_and_send_to_admins
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import check_unique_server_name, get_servers_from_db
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_key_on_cluster
@@ -369,7 +369,11 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery):
await callback_query.message.answer(
f"❌ Нет ключей для синхронизации в кластере {cluster_name}.",
reply_markup=InlineKeyboardBuilder()
.row(InlineKeyboardButton(text="🔙 Назад", callback_data="servers_editor"))
.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data="servers_editor"
)
)
.as_markup(),
)
return
@@ -408,7 +412,6 @@ async def sync_cluster_handler(callback_query: types.CallbackQuery):
await conn.close()
@router.callback_query(F.data.startswith("server_availability|"), IsAdminFilter())
async def handle_check_server_availability(callback_query: types.CallbackQuery):
cluster_name = callback_query.data.split("|")[1]
+74 -24
View File
@@ -7,9 +7,15 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import TOTAL_GB
from database import delete_user_data, get_client_id_by_email, get_servers_from_db, restore_trial, update_key_expiry
from database import (
delete_user_data,
get_client_id_by_email,
get_servers_from_db,
restore_trial,
update_key_expiry,
)
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
delete_key_from_cluster,
@@ -56,7 +62,7 @@ async def prompt_username(callback_query: CallbackQuery, state: FSMContext):
async def handle_username_input(
message: types.Message, state: FSMContext, session: Any
):
username = message.text.strip().lstrip('@').replace('https://t.me/', '')
username = message.text.strip().lstrip("@").replace("https://t.me/", "")
user_record = await session.fetchrow(
"SELECT tg_id FROM users WHERE username = $1", username
)
@@ -113,12 +119,19 @@ async def handle_username_input(
callback_data=f"restore_trial_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение",
callback_data=f"send_message_{tg_id}"
text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
@@ -201,8 +214,16 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session:
callback_data=f"change_balance_{tg_id}",
)
)
builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
builder.row(
InlineKeyboardButton(
text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник",
@@ -211,8 +232,7 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext, session:
)
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение",
callback_data=f"send_message_{tg_id}"
text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"
)
)
@@ -645,14 +665,29 @@ async def handle_user_info(
InlineKeyboardButton(text=f"🔑 {email}", callback_data=f"edit_key_{email}")
)
builder.row(InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"))
builder.row(InlineKeyboardButton(text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"))
builder.row(InlineKeyboardButton(text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"))
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение",
callback_data=f"send_message_{tg_id}"
text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="❌ Удалить клиента", callback_data=f"confirm_delete_user_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Обновить клиента", callback_data=f"user_info|{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="✉️ Отправить сообщение", callback_data=f"send_message_{tg_id}"
)
)
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor"))
@@ -671,19 +706,26 @@ async def handle_user_info(
@router.callback_query(F.data.startswith("confirm_delete_user_"), IsAdminFilter())
async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMContext, session: Any):
async def confirm_delete_user(
callback_query: types.CallbackQuery, state: FSMContext, session: Any
):
tg_id = int(callback_query.data.split("_")[3])
confirmation_markup = InlineKeyboardMarkup(
row_width=2,
inline_keyboard=[
[InlineKeyboardButton(text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}")],
[
InlineKeyboardButton(
text="✅ Подтвердить", callback_data=f"delete_user_{tg_id}"
)
],
[InlineKeyboardButton(text="❌ Отменить", callback_data="user_editor")],
],
)
await callback_query.message.answer(
f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?", reply_markup=confirmation_markup
f"Вы уверены, что хотите удалить пользователя с ID {tg_id}?",
reply_markup=confirmation_markup,
)
@@ -691,7 +733,9 @@ async def confirm_delete_user(callback_query: types.CallbackQuery, state: FSMCon
async def delete_user(callback_query: types.CallbackQuery, session: Any):
tg_id = int(callback_query.data.split("_")[2])
key_records = await session.fetch("SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id)
key_records = await session.fetch(
"SELECT email, client_id FROM keys WHERE tg_id = $1", tg_id
)
async def delete_keys_from_servers():
try:
@@ -702,7 +746,9 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any):
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}")
logger.error(
f"Ошибка при удалении ключей с серверов для пользователя {tg_id}: {e}"
)
await delete_keys_from_servers()
@@ -712,9 +758,13 @@ async def delete_user(callback_query: types.CallbackQuery, session: Any):
back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="user_editor")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard)
await callback_query.message.answer(
f"🗑️ Пользователь с ID {tg_id} был удален.", reply_markup=keyboard
)
except Exception as e:
logger.error(f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}")
logger.error(
f"Ошибка при удалении данных из базы данных для пользователя {tg_id}: {e}"
)
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении пользователя с ID {tg_id}. Попробуйте снова."
)
+1 -1
View File
@@ -3,8 +3,8 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from logger import logger
+19 -17
View File
@@ -5,9 +5,15 @@ import asyncpg
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CONNECT_MACOS, CONNECT_WINDOWS, DATABASE_URL, SUPPORT_CHAT_URL
from handlers.texts import CONNECT_TV_TEXT, INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE, SUBSCRIPTION_DETAILS_TEXT
from handlers.texts import (
CONNECT_TV_TEXT,
INSTRUCTION_PC,
INSTRUCTIONS,
KEY_MESSAGE,
SUBSCRIPTION_DETAILS_TEXT,
)
from logger import logger
router = Router()
@@ -15,13 +21,17 @@ router = Router()
@router.callback_query(F.data == "instructions")
@router.message(F.text == "/instructions")
async def send_instructions(callback_query_or_message: types.CallbackQuery | types.Message):
async def send_instructions(
callback_query_or_message: types.CallbackQuery | types.Message,
):
instructions_message = INSTRUCTIONS
image_path = os.path.join("img", "instructions.jpg")
if not os.path.isfile(image_path):
if isinstance(callback_query_or_message, types.CallbackQuery):
await callback_query_or_message.message.answer("Файл изображения не найден.")
await callback_query_or_message.message.answer(
"Файл изображения не найден."
)
else:
await callback_query_or_message.answer("Файл изображения не найден.")
return
@@ -45,7 +55,6 @@ async def send_instructions(callback_query_or_message: types.CallbackQuery | typ
)
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
@@ -101,15 +110,13 @@ async def process_connect_tv(callback_query: types.CallbackQuery):
text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
text=CONNECT_TV_TEXT,
reply_markup=builder.as_markup(),
parse_mode="HTML",
disable_web_page_preview=True
disable_web_page_preview=True,
)
@@ -136,8 +143,7 @@ async def process_continue_tv(callback_query: types.CallbackQuery):
finally:
await conn.close()
subscription_link = record['key']
subscription_link = record["key"]
message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link)
@@ -147,12 +153,8 @@ async def process_continue_tv(callback_query: types.CallbackQuery):
text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"
)
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
text=message_text,
reply_markup=builder.as_markup(),
parse_mode="HTML"
text=message_text, reply_markup=builder.as_markup(), parse_mode="HTML"
)
+63 -21
View File
@@ -8,8 +8,6 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
@@ -21,7 +19,15 @@ from config import (
TRIAL_TIME,
USE_NEW_PAYMENT_FLOW,
)
from database import get_balance, get_trial, save_temporary_data, store_key, update_balance
from bot import bot
from database import (
get_balance,
get_trial,
save_temporary_data,
store_key,
update_balance,
)
from handlers.buttons.add_subscribe import (
DOWNLOAD_ANDROID_BUTTON,
DOWNLOAD_IOS_BUTTON,
@@ -47,18 +53,25 @@ class Form(StatesGroup):
@router.callback_query(F.data == "create_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext, session: Any):
async def confirm_create_new_key(
callback_query: CallbackQuery, state: FSMContext, session: Any
):
tg_id = callback_query.message.chat.id
logger.info(f"User {tg_id} confirmed creation of a new key.")
logger.info(f"Balance for user {tg_id} is sufficient. Proceeding with key creation.")
logger.info(
f"Balance for user {tg_id} is sufficient. Proceeding with key creation."
)
await handle_key_creation(tg_id, state, session, callback_query)
async def handle_key_creation(
tg_id: int, state: FSMContext, session: Any, message_or_query: Message | CallbackQuery
tg_id: int,
state: FSMContext,
session: Any,
message_or_query: Message | CallbackQuery,
):
"""Создание ключа с учётом выбора тарифного плана."""
current_time = datetime.utcnow()
@@ -68,7 +81,9 @@ async def handle_key_creation(
expiry_time = current_time + timedelta(days=TRIAL_TIME)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
await session.execute("UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id)
await session.execute(
"UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
)
await create_key(tg_id, expiry_time, state, session, message_or_query)
else:
builder = InlineKeyboardBuilder()
@@ -85,14 +100,18 @@ async def handle_key_creation(
builder.row(
InlineKeyboardButton(
text=f"📅 {plan_id} мес. - {price}{discount_text}", callback_data=f"select_plan_{plan_id}"
text=f"📅 {plan_id} мес. - {price}{discount_text}",
callback_data=f"select_plan_{plan_id}",
)
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await message_or_query.message.answer(
"💳 Выберите тарифный план для создания нового ключа:", reply_markup=builder.as_markup()
"💳 Выберите тарифный план для создания нового ключа:",
reply_markup=builder.as_markup(),
)
await state.update_data(tg_id=tg_id)
await state.set_state(Form.waiting_for_server_selection)
@@ -120,7 +139,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
"plan_price": plan_price,
"duration_days": duration_days,
"required_amount": max(0, plan_price - balance),
}
},
)
if balance < plan_price:
@@ -130,8 +149,12 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
await process_custom_amount_input(callback_query, session)
else:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
builder.row(
InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay")
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
await callback_query.message.answer(
f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.",
@@ -143,6 +166,7 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
expiry_time = datetime.utcnow() + timedelta(days=duration_days)
await create_key(tg_id, expiry_time, None, session, callback_query)
async def create_key(
tg_id: int,
expiry_time: datetime,
@@ -162,7 +186,9 @@ async def create_key(
)
if not existing_key:
break
logger.warning(f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one.")
logger.warning(
f"Key name '{key_name}' already exists for user {tg_id}. Generating a new one."
)
client_id = str(uuid.uuid4())
email = key_name.lower()
@@ -187,13 +213,23 @@ async def create_key(
await asyncio.gather(*tasks)
logger.info(f"Key created on cluster {least_loaded_cluster} for user {tg_id}.")
await store_key(tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster, session)
await store_key(
tg_id,
client_id,
email,
expiry_timestamp,
public_link,
least_loaded_cluster,
session,
)
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id} on cluster: {e}")
if isinstance(message_or_query, Message):
await message_or_query.answer("❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.")
await message_or_query.answer(
"❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова."
)
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(
"❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова."
@@ -201,7 +237,7 @@ async def create_key(
else:
await bot.send_message(
chat_id=tg_id,
text="❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова."
text="❌ Произошла ошибка при создании ключа. Пожалуйста, попробуйте снова.",
)
return
@@ -213,12 +249,14 @@ async def create_key(
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"),
InlineKeyboardButton(
text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"
),
)
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
remaining_time = expiry_time - datetime.utcnow()
@@ -228,9 +266,13 @@ async def create_key(
if isinstance(message_or_query, Message):
await message_or_query.answer(key_message, reply_markup=builder.as_markup())
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(key_message, reply_markup=builder.as_markup())
await message_or_query.message.answer(
key_message, reply_markup=builder.as_markup()
)
else:
await bot.send_message(chat_id=tg_id, text=key_message, reply_markup=builder.as_markup())
await bot.send_message(
chat_id=tg_id, text=key_message, reply_markup=builder.as_markup()
)
if state:
await state.clear()
+2 -2
View File
@@ -1,9 +1,9 @@
import asyncio
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB
from py3xui import AsyncApi
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, TOTAL_GB
from database import get_servers_from_db
from logger import logger
@@ -11,7 +11,7 @@ from logger import logger
async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_timestamp):
"""
Создает ключ на всех серверах указанного кластера.
:param cluster_id: ID кластера.
:param tg_id: Telegram ID пользователя.
:param client_id: Уникальный идентификатор клиента.
+13 -19
View File
@@ -7,7 +7,6 @@ from typing import Any
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
@@ -18,6 +17,7 @@ from config import (
RENEWAL_PLANS,
TOTAL_GB,
)
from database import (
delete_key,
get_balance,
@@ -105,14 +105,10 @@ def build_keys_response(records):
)
builder.row(
InlineKeyboardButton(
text=" Добавить подписку", callback_data="create_key"
)
InlineKeyboardButton(text=" Добавить подписку", callback_data="create_key")
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
inline_keyboard = builder.as_markup()
response_message = (
@@ -122,14 +118,18 @@ def build_keys_response(records):
return inline_keyboard, response_message
async def send_with_optional_image(send_message, send_photo, image_path, text, keyboard):
async def send_with_optional_image(
send_message, send_photo, image_path, text, keyboard
):
"""
Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст.
"""
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await send_photo(
photo=BufferedInputFile(image_file.read(), filename=os.path.basename(image_path)),
photo=BufferedInputFile(
image_file.read(), filename=os.path.basename(image_path)
),
caption=text,
reply_markup=keyboard,
)
@@ -140,8 +140,6 @@ async def send_with_optional_image(send_message, send_photo, image_path, text, k
)
@router.callback_query(F.data.startswith("view_key|"))
async def process_callback_view_key(callback_query: types.CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
@@ -166,9 +164,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = (
"<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0"
)
days_left_message = "<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0\nОсталось минут: 0"
else:
total_seconds = int(time_left.total_seconds())
days = total_seconds // 86400
@@ -191,7 +187,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
builder.row(
InlineKeyboardButton(
text="🔄 Обновить подписку",
callback_data=f"update_subscription|{key_name}"
callback_data=f"update_subscription|{key_name}",
)
)
@@ -203,9 +199,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
)
builder.row(
InlineKeyboardButton(
text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}"
),
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key}"),
InlineKeyboardButton(
text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key}"
),
@@ -217,7 +211,7 @@ async def process_callback_view_key(callback_query: types.CallbackQuery, session
),
InlineKeyboardButton(
text=TV_BUTTON, callback_data=f"connect_tv|{key_name}"
)
),
)
builder.row(
+8 -9
View File
@@ -4,8 +4,8 @@ from datetime import datetime
import aiohttp
import asyncpg
from aiohttp import web
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, TRANSITION_DATE_STR
from database import get_servers_from_db
from logger import logger
@@ -95,7 +95,9 @@ async def handle_old_subscription(request):
status=400,
)
logger.info(f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}")
logger.info(
f"Значение created_at для клиента с email {email}: {created_at_ms}, кластер: {cluster_name}"
)
created_at_datetime = datetime.utcfromtimestamp(created_at_ms / 1000)
logger.info(
@@ -129,9 +131,8 @@ async def handle_old_subscription(request):
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": "base64:" + base64.b64encode(
encoded_project_name.encode("utf-8")
).decode("utf-8"),
"profile-title": "base64:"
+ base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
@@ -141,7 +142,6 @@ async def handle_old_subscription(request):
await conn.close()
async def handle_new_subscription(request):
email = request.match_info.get("email")
tg_id = request.match_info.get("tg_id")
@@ -205,9 +205,8 @@ async def handle_new_subscription(request):
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": "base64:" + base64.b64encode(
encoded_project_name.encode("utf-8")
).decode("utf-8"),
"profile-title": "base64:"
+ base64.b64encode(encoded_project_name.encode("utf-8")).decode("utf-8"),
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
+9 -3
View File
@@ -4,10 +4,17 @@ from datetime import datetime, timedelta
from typing import Any
import pytz
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
LIMIT_IP,
PUBLIC_LINK,
TOTAL_GB,
TRIAL_TIME,
)
from py3xui import AsyncApi
from client import add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, TOTAL_GB, TRIAL_TIME
from database import get_servers_from_db, store_key, use_trial
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
@@ -17,8 +24,7 @@ from logger import logger
async def create_trial_key(tg_id: int, session: Any):
try:
trial_status = await session.fetchval(
"SELECT trial FROM connections WHERE tg_id = $1",
tg_id
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
if trial_status == 1:
+43 -22
View File
@@ -5,8 +5,6 @@ import asyncpg
from aiogram import Bot, Router, types
from aiogram.exceptions import TelegramForbiddenError
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
@@ -16,6 +14,8 @@ from config import (
TOTAL_GB,
TRIAL_TIME,
)
from py3xui import AsyncApi
from database import (
add_blocked_user,
add_notification,
@@ -140,7 +140,11 @@ async def notify_10h_keys(
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB
cluster_id,
email,
record["client_id"],
new_expiry_time,
TOTAL_GB,
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
@@ -169,7 +173,9 @@ async def notify_10h_keys(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except TelegramForbiddenError:
logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.")
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
@@ -183,7 +189,9 @@ async def notify_10h_keys(
keyboard.button(text="👤 Личный кабинет", callback_data="profile")
keyboard.adjust(1)
await bot.send_message(tg_id, message, reply_markup=keyboard.as_markup())
await bot.send_message(
tg_id, message, reply_markup=keyboard.as_markup()
)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
await conn.execute(
@@ -191,9 +199,12 @@ async def notify_10h_keys(
record["client_id"],
)
logger.info(
f"Обновлено поле notified для клиента {record['client_id']}.")
f"Обновлено поле notified для клиента {record['client_id']}."
)
except TelegramForbiddenError:
logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.")
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.debug(
@@ -260,7 +271,11 @@ async def notify_24h_keys(
for cluster_id in servers:
await renew_key_in_cluster(
cluster_id, email, record["client_id"], new_expiry_time, TOTAL_GB
cluster_id,
email,
record["client_id"],
new_expiry_time,
TOTAL_GB,
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
@@ -290,7 +305,9 @@ async def notify_24h_keys(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except TelegramForbiddenError:
logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.")
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при продлении подписки для клиента {tg_id}: {e}")
@@ -317,11 +334,11 @@ async def notify_24h_keys(
)
keyboard = builder.as_markup()
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(
f"Уведомление за 24 часа отправлено пользователю {tg_id}."
)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except TelegramForbiddenError:
logger.warning(f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users.")
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Записываем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(
@@ -332,9 +349,7 @@ async def notify_24h_keys(
"UPDATE keys SET notified_24h = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(
f"Обновлено поле notified_24h для клиента {record['client_id']}."
)
logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.")
await asyncio.gather(*(process_record(record) for record in records_24h))
logger.info("Обработка всех уведомлений за 24 часа завершена.")
@@ -389,14 +404,20 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
try:
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Отправлено уведомление неактивному пользователю {tg_id}.")
logger.info(
f"Отправлено уведомление неактивному пользователю {tg_id}."
)
await add_notification(tg_id, "inactive_trial", session=conn)
except TelegramForbiddenError:
logger.warning(f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users.")
logger.warning(
f"Бот заблокирован пользователем {tg_id}. Добавляем в blocked_users."
)
await add_blocked_user(tg_id, conn)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
logger.error(
f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
)
except Exception as e:
logger.error(f"Ошибка при обработке пользователя {tg_id}: {e}")
@@ -418,7 +439,9 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
current_time,
)
logger.info(f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает.")
logger.info(
f"Найдено {len(expiring_keys)} подписок, срок действия которых скоро истекает."
)
for record in expiring_keys:
try:
@@ -427,7 +450,6 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
logger.error(f"Ошибка при обработке подписки {record['client_id']}: {e}")
async def process_key(record, bot, conn):
tg_id = record["tg_id"]
client_id = record["client_id"]
@@ -519,7 +541,6 @@ async def process_key(record, bot, conn):
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
async def check_online_users():
servers = await get_servers_from_db()
+8 -3
View File
@@ -1,7 +1,6 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CRYPTO_BOT_ENABLE,
DONATIONS_ENABLE,
@@ -54,9 +53,15 @@ async def handle_pay(callback_query: CallbackQuery):
)
)
builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon"))
builder.row(
InlineKeyboardButton(
text="🎟️ Активировать купон", callback_data="activate_coupon"
)
)
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
-1
View File
@@ -1,7 +1,6 @@
__all__ = ("router",)
from aiogram import Router
from config import (
CRYPTO_BOT_ENABLE,
ROBOKASSA_ENABLE,
+2 -2
View File
@@ -7,8 +7,6 @@ from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from robokassa import HashAlgorithm, Robokassa
from config import (
ROBOKASSA_ENABLE,
ROBOKASSA_LOGIN,
@@ -16,6 +14,8 @@ from config import (
ROBOKASSA_PASSWORD2,
ROBOKASSA_TEST_MODE,
)
from robokassa import HashAlgorithm, Robokassa
from database import (
add_connection,
add_payment,
+18 -11
View File
@@ -5,10 +5,18 @@ from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL, NEWS_MESSAGE, RENEWAL_PLANS
from database import get_balance, get_key_count, get_referral_stats, get_trial
from handlers.buttons.profile import ADD_SUB, GIFTS, INSTRUCTIONS, INVITE, MAIN_MENU, MY_SUBS, PAYMENT
from handlers.buttons.profile import (
ADD_SUB,
GIFTS,
INSTRUCTIONS,
INVITE,
MAIN_MENU,
MY_SUBS,
PAYMENT,
)
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
router = Router()
@@ -17,7 +25,9 @@ router = Router()
@router.callback_query(F.data == "profile")
@router.message(F.text == "/profile")
async def process_callback_view_profile(
callback_query_or_message: types.Message | types.CallbackQuery, state: FSMContext, admin: bool
callback_query_or_message: types.Message | types.CallbackQuery,
state: FSMContext,
admin: bool,
):
if isinstance(callback_query_or_message, types.CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
@@ -38,7 +48,9 @@ async def process_callback_view_profile(
try:
trial_status = await get_trial(chat_id, conn)
profile_message = profile_message_send(username, chat_id, int(balance), key_count)
profile_message = profile_message_send(
username, chat_id, int(balance), key_count
)
if key_count == 0:
profile_message += "\n<pre>🔧 <i>Нажмите кнопку ➕ Устройство, чтобы настроить VPN-подключение</i></pre>"
@@ -47,15 +59,10 @@ async def process_callback_view_profile(
builder = InlineKeyboardBuilder()
if trial_status == 0 or key_count == 0:
builder.row(
InlineKeyboardButton(text=ADD_SUB, callback_data="create_key")
)
builder.row(InlineKeyboardButton(text=ADD_SUB, callback_data="create_key"))
else:
builder.row(
InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys")
)
builder.row(InlineKeyboardButton(text=MY_SUBS, callback_data="view_keys"))
builder.row(
InlineKeyboardButton(
+68 -31
View File
@@ -11,7 +11,6 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
CHANNEL_EXISTS,
CHANNEL_URL,
@@ -22,7 +21,14 @@ from config import (
DOWNLOAD_IOS,
SUPPORT_CHAT_URL,
)
from database import add_connection, add_referral, check_connection_exists, get_trial, use_trial
from database import (
add_connection,
add_referral,
check_connection_exists,
get_trial,
use_trial,
)
from handlers.buttons.add_subscribe import (
DOWNLOAD_ANDROID_BUTTON,
DOWNLOAD_IOS_BUTTON,
@@ -58,10 +64,11 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
connection_exists = await check_connection_exists(message.chat.id)
logger.info(f"Проверка существования подключения: {connection_exists}")
if not connection_exists:
await add_connection(tg_id=message.chat.id, session=session)
logger.info(f"Пользователь {message.chat.id} успешно добавлен в базу данных.")
logger.info(
f"Пользователь {message.chat.id} успешно добавлен в базу данных."
)
if "gift_" in message.text:
logger.info(f"Обнаружена ссылка на подарок: {message.text}")
@@ -71,28 +78,41 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
recipient_tg_id = message.chat.id
gift_info = await session.fetchrow(
"SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE", gift_id
"SELECT * FROM gifts WHERE gift_id = $1 AND is_used = FALSE",
gift_id,
)
if gift_info is None:
logger.warning(f"Подарок с ID {gift_id} уже был использован или не существует.")
await message.answer("Этот подарок уже был использован или не существует.")
logger.warning(
f"Подарок с ID {gift_id} уже был использован или не существует."
)
await message.answer(
"Этот подарок уже был использован или не существует."
)
return await show_start_menu(message, admin, session)
if gift_info['sender_tg_id'] == recipient_tg_id:
if gift_info["sender_tg_id"] == recipient_tg_id:
logger.warning(
f"Пользователь {recipient_tg_id} попытался активировать подарок, который был отправлен им самим."
)
await message.answer("❌ Вы не можете получить подарок от самого себя.")
await message.answer(
"❌ Вы не можете получить подарок от самого себя."
)
return await show_start_menu(message, admin, session)
selected_months = gift_info['selected_months']
expiry_time = gift_info['expiry_time']
selected_months = gift_info["selected_months"]
expiry_time = gift_info["expiry_time"]
expiry_time_naive = expiry_time.replace(tzinfo=None)
logger.info(f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}.")
logger.info(
f"Подарок с ID {gift_id} успешно найден для пользователя {recipient_tg_id}."
)
await create_key(recipient_tg_id, expiry_time_naive, state, session, message)
logger.info(f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев.")
await create_key(
recipient_tg_id, expiry_time_naive, state, session, message
)
logger.info(
f"Ключ создан для пользователя {recipient_tg_id} на срок {selected_months} месяцев."
)
await session.execute(
"UPDATE gifts SET is_used = TRUE, recipient_tg_id = $1 WHERE gift_id = $2",
@@ -103,7 +123,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await message.answer(
f"🎉 Ваш подарок на {selected_months} {'месяц' if selected_months == 1 else 'месяца' if selected_months in [2, 3, 4] else 'месяцев'} активирован!"
)
logger.info(f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}.")
logger.info(
f"Подарок на {selected_months} месяцев активирован для пользователя {recipient_tg_id}."
)
return
elif "referral_" in message.text:
@@ -114,16 +136,23 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
logger.info(
f"Пользователь {message.chat.id} уже зарегистрирован и не может стать рефералом."
)
await message.answer("❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку.")
await message.answer(
"❌ Вы уже зарегистрированы и не можете использовать реферальную ссылку."
)
return await show_start_menu(message, admin, session)
if referrer_tg_id == message.chat.id:
logger.warning(f"Пользователь {message.chat.id} попытался стать рефералом самого себя.")
await message.answer("❌ Вы не можете быть рефералом самого себя.")
logger.warning(
f"Пользователь {message.chat.id} попытался стать рефералом самого себя."
)
await message.answer(
"❌ Вы не можете быть рефералом самого себя."
)
return await show_start_menu(message, admin, session)
existing_referral = await session.fetchrow(
"SELECT * FROM referrals WHERE referred_tg_id = $1", message.chat.id
"SELECT * FROM referrals WHERE referred_tg_id = $1",
message.chat.id,
)
if existing_referral:
@@ -131,7 +160,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
return await show_start_menu(message, admin, session)
await add_referral(message.chat.id, referrer_tg_id, session)
logger.info(f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}")
logger.info(
f"Реферал {message.chat.id} использовал ссылку от пользователя {referrer_tg_id}"
)
return await show_start_menu(message, admin, session)
except (ValueError, IndexError) as e:
@@ -139,12 +170,16 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
return
else:
logger.info(f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка.")
logger.info(
f"Пользователь {message.chat.id} зашел без реферальной ссылки или подарка."
)
await show_start_menu(message, admin, session)
except (ValueError, IndexError) as e:
logger.error(f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}")
logger.error(
f"Ошибка при обработке сообщения пользователя {message.chat.id}: {e}"
)
await message.answer("❌ Произошла ошибка. Пожалуйста, попробуйте снова.")
else:
await show_start_menu(message, admin, session)
@@ -159,7 +194,9 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn"))
builder.row(
InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
@@ -169,7 +206,9 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
)
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(
InlineKeyboardButton(text="🔧 Администратор", callback_data="admin")
)
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
@@ -223,12 +262,8 @@ async def handle_connect_vpn(callback_query: CallbackQuery, session: Any):
),
)
builder.row(
InlineKeyboardButton(
text=PC_BUTTON, callback_data=f"connect_pc|{email}"
),
InlineKeyboardButton(
text=TV_BUTTON, callback_data=f"connect_tv|{email}"
)
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile")
@@ -244,7 +279,9 @@ async def handle_about_vpn(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
+5 -3
View File
@@ -4,9 +4,9 @@ import re
import aiohttp
import asyncpg
from config import DATABASE_URL
from bot import bot
from config import DATABASE_URL
from database import get_servers_from_db
from logger import logger
@@ -14,10 +14,12 @@ from logger import logger
async def get_usd_rate():
try:
async with aiohttp.ClientSession() as session:
async with session.get('https://www.cbr-xml-daily.ru/daily_json.js') as response:
async with session.get(
"https://www.cbr-xml-daily.ru/daily_json.js"
) as response:
if response.status == 200:
data = await response.text()
usd = float(json.loads(data)['Valute']['USD']['Value'])
usd = float(json.loads(data)["Valute"]["USD"]["Value"])
else:
usd = float(100) # Default value if request fails
except Exception as e:
-1
View File
@@ -3,7 +3,6 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
-1
View File
@@ -4,7 +4,6 @@ from typing import Any
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+42
View File
@@ -0,0 +1,42 @@
import asyncio
from aiogram import Dispatcher, types
from aiogram.dispatcher import DEFAULT_RATE_LIMIT
from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
from aiogram.utils.keyboard import InlineKeyboardBuilder
class ThrottlingMiddleware(BaseMiddleware):
def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix="antiflood_"):
self.rate_limit = limit
self.prefix = key_prefix
super(ThrottlingMiddleware, self).__init__()
async def on_process_message(self, message: types.Message, data: dict):
handler = current_handler.get()
dispatcher = Dispatcher.get_current()
if handler:
limit = getattr(handler, "throttling_rate_limit", self.rate_limit)
key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}")
else:
limit = self.rate_limit
key = f"{self.prefix}_message"
try:
await dispatcher.throttle(key, rate=limit)
except Throttled as t:
await self.message_throttled(message, t)
raise CancelHandler()
async def message_throttled(self, message: types.Message, throttled: Throttled):
delta = throttled.rate - throttled.delta
if throttled.exceeded_count <= 2:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message.reply("🚫 Слишком много запросов! Пожалуйста, не торопитесь!", reply_markup=builder.as_markup())
await asyncio.sleep(delta)
+4 -3
View File
@@ -5,10 +5,10 @@ from datetime import datetime, timedelta
import asyncpg
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from ping3 import ping
from bot import bot
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from database import get_servers_from_db
from logger import logger
@@ -104,8 +104,9 @@ async def notify_admin(server_name: str):
current_time = datetime.now()
last_notification_time = last_notification_times.get(server_name)
if last_notification_time and current_time - last_notification_time < timedelta(
minutes=3
if (
last_notification_time
and current_time - last_notification_time < timedelta(minutes=3)
):
logger.info(
f"Не отправляем уведомление для сервера {server_name}, так как прошло менее 3 минут."