edit_text/lots of improvements

This commit is contained in:
Vladless
2025-02-16 07:17:35 +03:00
parent 4a14515a63
commit 469d2c9afe
35 changed files with 5881 additions and 3618 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, PG_HOST, PG_PORT
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER, PG_HOST, PG_PORT
from logger import logger
+2 -2
View File
@@ -8,8 +8,8 @@ from aiogram.filters import ExceptionTypeFilter
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import BufferedInputFile, ErrorEvent
from aiogram.utils.markdown import hbold
from config import ADMIN_ID, API_TOKEN
from config import ADMIN_ID, API_TOKEN
from filters.private import IsPrivateFilter
from logger import logger
from middlewares import register_middleware
@@ -18,7 +18,7 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
version = "4.0.0-Alpha(13-dev)"
version = "4.0.0-beta(16)"
register_middleware(dp)
+1 -1
View File
@@ -2,8 +2,8 @@ from dataclasses import dataclass
from typing import Any
import py3xui
from config import LIMIT_IP, SUPERNODE
from config import LIMIT_IP, SUPERNODE
from logger import logger
+1 -1
View File
@@ -4,8 +4,8 @@ from typing import Any
import asyncpg
import pytz
from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from config import CASHBACK, CHECK_REFERRAL_REWARD_ISSUED, DATABASE_URL, REFERRAL_BONUS_PERCENTAGES
from logger import logger
+1
View File
@@ -1,5 +1,6 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
+1 -1
View File
@@ -4,8 +4,8 @@ from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from config import USERNAME_BOT
from config import USERNAME_BOT
from database import create_coupon, delete_coupon, get_all_coupons
from filters.admin import IsAdminFilter
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback, build_coupons_kb, build_coupons_list_kb
+1 -1
View File
@@ -6,10 +6,10 @@ from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
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, delete_server, get_servers
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import create_key_on_cluster
+1 -1
View File
@@ -7,8 +7,8 @@ from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, Message
from config import TOTAL_GB
from config import TOTAL_GB
from database import (
delete_key,
delete_user_data,
+1 -1
View File
@@ -6,8 +6,8 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CAPTCHA_EMOJIS
from config import CAPTCHA_EMOJIS
from logger import logger
router = Router()
+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 CallbackQuery, InlineKeyboardButton, LabeledPrice, Message, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RUB_TO_XTR
from config import RUB_TO_XTR
from logger import logger
+39 -43
View File
@@ -1,12 +1,15 @@
import os
from typing import Any
import aiofiles
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
from config import CONNECT_MACOS, CONNECT_WINDOWS, SUPPORT_CHAT_URL
from database import get_key_details
from handlers.texts import (
CONNECT_TV_TEXT,
@@ -15,70 +18,63 @@ from handlers.texts import (
KEY_MESSAGE,
SUBSCRIPTION_DETAILS_TEXT,
)
from logger import logger
from handlers.utils import edit_or_send_message
router = Router()
@router.callback_query(F.data == "instructions")
@router.message(F.text == "/instructions")
async def send_instructions(
callback_query_or_message: CallbackQuery | Message,
):
async def send_instructions(callback_query_or_message: CallbackQuery | 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, CallbackQuery):
await callback_query_or_message.message.answer("Файл изображения не найден.")
else:
await callback_query_or_message.answer("Файл изображения не найден.")
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"),
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if isinstance(callback_query_or_message, CallbackQuery):
send_photo = callback_query_or_message.message.answer_photo
target_message = callback_query_or_message.message
else:
send_photo = callback_query_or_message.answer_photo
target_message = callback_query_or_message
async with aiofiles.open(image_path, "rb") as image_from_buffer:
image_data = await image_from_buffer.read()
await send_photo(
BufferedInputFile(image_data, filename="instructions.jpg"),
caption=instructions_message,
reply_markup=builder.as_markup(),
)
await edit_or_send_message(
target_message=target_message,
text=instructions_message,
reply_markup=builder.as_markup(),
media_path=image_path,
)
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
record = await get_key_details(key_name, session)
if not record:
await callback_query.message.answer("❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍")
await edit_or_send_message(
target_message=callback_query.message,
text="❌ <b>Ключ не найден. Проверьте имя ключа.</b> 🔍",
reply_markup=types.InlineKeyboardMarkup(),
media_path=None,
)
return
key = record["key"]
key_message = KEY_MESSAGE.format(key)
instruction_message = f"{key_message}{INSTRUCTION_PC}"
key_message_text = KEY_MESSAGE.format(key)
instruction_message = f"{key_message_text}{INSTRUCTION_PC}"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"))
builder.row(InlineKeyboardButton(text="💻 Подключить MacOS", url=f"{CONNECT_MACOS}{key}"))
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"))
builder.row(InlineKeyboardButton(text="🆘 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
instruction_message,
await edit_or_send_message(
target_message=callback_query.message,
text=instruction_message,
reply_markup=builder.as_markup(),
disable_web_page_preview=True,
media_path=None,
)
@@ -88,11 +84,14 @@ async def process_connect_tv(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="▶ Продолжить", callback_data=f"continue_tv|{key_name}"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
await edit_or_send_message(
target_message=callback_query.message,
text=CONNECT_TV_TEXT,
reply_markup=builder.as_markup(),
media_path=None,
disable_web_page_preview=True,
)
@@ -102,18 +101,15 @@ async def process_continue_tv(callback_query: CallbackQuery, session: Any):
key_name = callback_query.data.split("|")[1]
tg_id = callback_query.from_user.id
logger.info(f"tg_id: {tg_id}, key_name: {key_name}")
record = await get_key_details(key_name, session)
logger.info(f"Query result: {record}")
subscription_link = record["key"]
message_text = SUBSCRIPTION_DETAILS_TEXT.format(subscription_link=subscription_link)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📖 Полная инструкция", url="https://vpn4tv.com/quick-guide.html"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{key_name}"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(text=message_text, reply_markup=builder.as_markup())
await edit_or_send_message(
target_message=callback_query.message, text=message_text, reply_markup=builder.as_markup(), media_path=None
)
+80 -55
View File
@@ -8,11 +8,16 @@ from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from py3xui import AsyncApi
from bot import bot
from client import delete_client
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
CONNECT_ANDROID,
CONNECT_IOS,
CONNECT_PHONE_BUTTON,
DOWNLOAD_ANDROID,
DOWNLOAD_IOS,
NOTIFY_EXTRA_DAYS,
@@ -24,10 +29,6 @@ from config import (
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
)
from py3xui import AsyncApi
from bot import bot
from client import delete_client
from database import (
add_connection,
check_connection_exists,
@@ -52,7 +53,7 @@ from handlers.keys.key_utils import create_client_on_server, create_key_on_clust
from handlers.payments.robokassa_pay import handle_custom_amount_input
from handlers.payments.yookassa_pay import process_custom_amount_input
from handlers.texts import DISCOUNTS, key_message_success
from handlers.utils import generate_random_email, get_least_loaded_cluster
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster
from logger import logger
router = Router()
@@ -84,13 +85,10 @@ async def handle_key_creation(
if not TRIAL_TIME_DISABLE:
trial_status = await get_trial(tg_id, session)
if trial_status in [0, -1]:
extra_days = NOTIFY_EXTRA_DAYS if trial_status == -1 else 0
expiry_time = current_time + timedelta(days=TRIAL_TIME + extra_days)
logger.info(f"Доступен {TRIAL_TIME + extra_days}-дневный пробный период пользователю {tg_id}.")
updated = await update_trial(tg_id, 1, session)
if updated:
await create_key(tg_id, expiry_time, state, session, message_or_query)
@@ -114,9 +112,16 @@ async def handle_key_creation(
)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await message_or_query.message.answer(
"💳 Выберите тарифный план для создания нового ключа:",
if isinstance(message_or_query, CallbackQuery):
target_message = message_or_query.message
else:
target_message = message_or_query
await edit_or_send_message(
target_message=target_message,
text="💳 Выберите тарифный план для создания нового ключа:",
reply_markup=builder.as_markup(),
media_path=None,
)
await state.update_data(tg_id=tg_id)
@@ -154,11 +159,22 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any):
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.",
await edit_or_send_message(
target_message=callback_query.message,
text=f"💳 Недостаточно средств. Для продолжения необходимо пополнить баланс на {required_amount}₽.",
reply_markup=builder.as_markup(),
media_path=None,
)
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⏳ Подождите...", callback_data="creating_key"))
await edit_or_send_message(
target_message=callback_query.message,
text="⏳ Подождите, создаем вам подключение...",
reply_markup=builder.as_markup(),
)
expiry_time = datetime.now(moscow_tz) + timedelta(days=duration_days)
await create_key(tg_id, expiry_time, None, session, callback_query, None, plan_id)
await update_balance(tg_id, -plan_price, session)
@@ -175,6 +191,7 @@ async def create_key(
):
"""Создаёт ключ с заданным сроком действия."""
target_message = message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
if not await check_connection_exists(tg_id):
await add_connection(tg_id, balance=0.0, trial=0, session=session)
logger.info(f"[Connection] Подключение создано для пользователя {tg_id}")
@@ -202,25 +219,19 @@ async def create_key(
builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="profile"))
if isinstance(message_or_query, Message):
await message_or_query.answer(
"🌍 Пожалуйста, выберите страну для вашего ключа:",
if target_message:
await edit_or_send_message(
target_message=target_message,
text="🌍 Пожалуйста, выберите страну для вашего ключа:",
reply_markup=builder.as_markup(),
media_path=None,
)
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(
"🌍 Пожалуйста, выберите страну для вашего ключа:",
reply_markup=builder.as_markup(),
)
elif tg_id is not None:
else:
await bot.send_message(
chat_id=tg_id,
text="🌍 Пожалуйста, выберите страну для вашего ключа:",
reply_markup=builder.as_markup(),
)
else:
logger.error("[Country Selection] Невозможно определить идентификатор чата. Сообщение не отправлено.")
return
while True:
@@ -259,42 +270,48 @@ async def create_key(
except Exception as e:
logger.error(f"[Error] Ошибка при создании ключа для пользователя {tg_id}: {e}")
error_message = "❌ Произошла ошибка при создании подписки. Пожалуйста, попробуйте снова."
if isinstance(message_or_query, Message):
await message_or_query.answer(error_message)
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(error_message)
if target_message:
await edit_or_send_message(
target_message=target_message, text=error_message, reply_markup=None, media_path=None
)
else:
await bot.send_message(chat_id=tg_id, text=error_message)
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"),
)
if CONNECT_PHONE_BUTTON:
builder.row(InlineKeyboardButton(text="📱 Подключить телефон", callback_data=f"connect_phone|{key_name}"))
else:
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key_name}"),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_name}"),
)
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"))
expiry_time = expiry_time.replace(tzinfo=None).astimezone(moscow_tz)
remaining_time = expiry_time - datetime.now(moscow_tz)
expiry_time_local = expiry_time.replace(tzinfo=None).astimezone(moscow_tz)
remaining_time = expiry_time_local - datetime.now(moscow_tz)
days = remaining_time.days
key_message_text = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
if isinstance(message_or_query, Message):
await message_or_query.answer(key_message_text, reply_markup=builder.as_markup())
elif isinstance(message_or_query, CallbackQuery):
await message_or_query.message.answer(key_message_text, reply_markup=builder.as_markup())
if target_message:
await edit_or_send_message(
target_message=target_message, text=key_message_text, reply_markup=builder.as_markup(), media_path=None
)
else:
await bot.send_message(chat_id=tg_id, text=key_message_text, reply_markup=builder.as_markup())
await bot.send_message(
chat_id=tg_id,
text=key_message_text,
reply_markup=builder.as_markup(),
)
if state:
await state.clear()
@@ -327,8 +344,11 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
builder.row(InlineKeyboardButton(text=country, callback_data=callback_data))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{old_key_name}"))
await callback_query.message.answer(
"🌍 Пожалуйста, выберите новую локацию для вашей подписки:", reply_markup=builder.as_markup()
await edit_or_send_message(
target_message=callback_query.message,
text="🌍 Пожалуйста, выберите новую локацию для вашей подписки:",
reply_markup=builder.as_markup(),
media_path=None,
)
except Exception as e:
logger.error(f"Ошибка при смене локации для пользователя {callback_query.from_user.id}: {e}")
@@ -434,14 +454,17 @@ async def finalize_key_creation(
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💬 Поддержка", url=SUPPORT_CHAT_URL))
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{public_link}"),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{public_link}"),
)
if CONNECT_PHONE_BUTTON:
builder.row(InlineKeyboardButton(text="📱 Подключить телефон", callback_data=f"connect_phone|{key_name}"))
else:
builder.row(
InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS),
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
)
builder.row(
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{key_name}"),
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_name}"),
)
builder.row(
InlineKeyboardButton(text=PC_BUTTON, callback_data=f"connect_pc|{email}"),
InlineKeyboardButton(text=TV_BUTTON, callback_data=f"connect_tv|{email}"),
@@ -452,7 +475,9 @@ async def finalize_key_creation(
days = remaining_time.days
key_message_text = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
await callback_query.message.answer(key_message_text, reply_markup=builder.as_markup())
await edit_or_send_message(
target_message=callback_query.message, text=key_message_text, reply_markup=builder.as_markup(), media_path=None
)
if state:
await state.clear()
+1 -1
View File
@@ -1,10 +1,10 @@
import asyncio
from typing import Any
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION
from py3xui import AsyncApi
from client import ClientConfig, add_client, delete_client, extend_client_key, get_client_traffic, toggle_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, USE_COUNTRY_SELECTION
from database import get_servers, store_key
from handlers.utils import get_least_loaded_cluster
from logger import logger
+53 -71
View File
@@ -4,12 +4,13 @@ import os
from datetime import datetime, timedelta
from typing import Any
import aiofiles
import asyncpg
import pytz
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CONNECT_ANDROID,
CONNECT_IOS,
@@ -25,8 +26,6 @@ from config import (
USE_COUNTRY_SELECTION,
USE_NEW_PAYMENT_FLOW,
)
from bot import bot
from database import (
check_server_name_by_cluster,
create_temporary_data,
@@ -62,7 +61,7 @@ from handlers.texts import (
SUCCESS_RENEWAL_MSG,
key_message,
)
from handlers.utils import handle_error
from handlers.utils import edit_or_send_message, handle_error
from logger import logger
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
@@ -74,24 +73,21 @@ router = Router()
@router.message(F.text == "/subs")
async def process_callback_or_message_view_keys(callback_query_or_message: Message | CallbackQuery, session: Any):
if isinstance(callback_query_or_message, CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
send_message = callback_query_or_message.message.answer
send_photo = callback_query_or_message.message.answer_photo
target_message = callback_query_or_message.message
else:
chat_id = callback_query_or_message.chat.id
send_message = callback_query_or_message.answer
send_photo = callback_query_or_message.answer_photo
target_message = callback_query_or_message
try:
records = await get_keys(chat_id, session)
records = await get_keys(target_message.chat.id, session)
inline_keyboard, response_message = build_keys_response(records)
image_path = os.path.join("img", "pic_keys.jpg")
await send_with_optional_image(send_message, send_photo, image_path, response_message, inline_keyboard)
await edit_or_send_message(
target_message=target_message, text=response_message, reply_markup=inline_keyboard, media_path=image_path
)
except Exception as e:
error_message = f"Ошибка при получении ключей: {e}"
await send_message(text=error_message)
await target_message.answer(text=error_message)
def build_keys_response(records):
@@ -134,32 +130,12 @@ def build_keys_response(records):
return inline_keyboard, response_message
async def send_with_optional_image(send_message, send_photo, image_path, text, keyboard):
"""
Отправляет сообщение с изображением, если файл существует. В противном случае отправляет только текст.
"""
if os.path.isfile(image_path):
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await send_photo(
photo=BufferedInputFile(image_data, filename=os.path.basename(image_path)),
caption=text,
reply_markup=keyboard,
)
else:
await send_message(
text=text,
reply_markup=keyboard,
)
@router.callback_query(F.data.startswith("view_key|"))
async def process_callback_view_key(callback_query: CallbackQuery, session: Any):
tg_id = callback_query.message.chat.id
key_name = callback_query.data.split("|")[1]
try:
record = await get_key_details(key_name, session)
if record:
key = record["key"]
expiry_time = record["expiry_time"]
@@ -176,11 +152,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>{days}</b> дней, <b>{hours}</b> часов, <b>{minutes}</b> минут"
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(
key, formatted_expiry_date, days_left_message, server_name, country if USE_COUNTRY_SELECTION else None
)
@@ -231,24 +205,20 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
keyboard = builder.as_markup()
image_path = os.path.join("img", "pic_view.jpg")
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
return
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_data, filename="pic_view.jpg"),
caption=response_message,
reply_markup=keyboard,
)
else:
await callback_query.message.answer(
text="<b>Информация о подписке не найдена.</b>",
await edit_or_send_message(
target_message=callback_query.message,
text=response_message,
reply_markup=keyboard,
media_path=image_path,
)
else:
await callback_query.message.answer(text="<b>Информация о подписке не найдена.</b>")
except Exception as e:
await handle_error(
tg_id,
@@ -298,7 +268,9 @@ async def process_callback_connect_phone(callback_query: CallbackQuery):
builder.row(InlineKeyboardButton(text="📖 Ручная установка", callback_data="instructions"))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data=f"view_key|{email}"))
await callback_query.message.answer(description, reply_markup=builder.as_markup())
await edit_or_send_message(
target_message=callback_query.message, text=description, reply_markup=builder.as_markup(), media_path=None
)
@router.callback_query(F.data.startswith("update_subscription|"))
@@ -330,13 +302,17 @@ async def process_callback_delete_key(callback_query: CallbackQuery):
]
)
await callback_query.message.answer(
text="<b>Вы уверены, что хотите удалить ключ?</b>",
reply_markup=confirmation_keyboard,
)
if callback_query.message.caption:
await callback_query.message.edit_caption(
caption="<b>Вы уверены, что хотите удалить ключ?</b>", reply_markup=confirmation_keyboard
)
else:
await callback_query.message.edit_text(
text="<b>Вы уверены, что хотите удалить ключ?</b>", reply_markup=confirmation_keyboard
)
except Exception as e:
logger.error(e)
logger.error(f"Ошибка при обработке запроса на удаление ключа {client_id}: {e}")
@router.callback_query(F.data.startswith("renew_key|"))
@@ -345,7 +321,6 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any
key_name = callback_query.data.split("|")[1]
try:
record = await get_key_details(key_name, session)
if record:
client_id = record["client_id"]
expiry_time = record["expiry_time"]
@@ -375,9 +350,11 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S"),
)
await callback_query.message.answer(
await edit_or_send_message(
target_message=callback_query.message,
text=response_message,
reply_markup=builder.as_markup(),
media_path=None,
)
else:
await callback_query.message.answer("<b>Ключ не найден.</b>")
@@ -390,7 +367,6 @@ async def process_callback_confirm_delete(callback_query: CallbackQuery, session
email = callback_query.data.split("|")[1]
try:
record = await get_key_details(email, session)
if record:
client_id = record["client_id"]
response_message = "Ключ успешно удален."
@@ -398,9 +374,9 @@ async def process_callback_confirm_delete(callback_query: CallbackQuery, session
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await delete_key(client_id, session)
await callback_query.message.answer(
response_message,
reply_markup=keyboard,
await edit_or_send_message(
target_message=callback_query.message, text=response_message, reply_markup=keyboard, media_path=None
)
servers = await get_servers(session)
@@ -410,24 +386,19 @@ async def process_callback_confirm_delete(callback_query: CallbackQuery, session
tasks = []
for cluster_id, _cluster in servers.items():
tasks.append(delete_key_from_cluster(cluster_id, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id}: {e}")
asyncio.create_task(delete_key_from_servers())
await delete_key(client_id, session)
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text="Назад", callback_data="view_keys")
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await callback_query.message.answer(
response_message,
reply_markup=keyboard,
await edit_or_send_message(
target_message=callback_query.message, text=response_message, reply_markup=keyboard, media_path=None
)
except Exception as e:
logger.error(e)
@@ -492,9 +463,11 @@ async def process_callback_renew_plan(callback_query: CallbackQuery, session: An
builder.row(InlineKeyboardButton(text="💳 Пополнить баланс", callback_data="pay"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
f"💳 Недостаточно средств. Пополните баланс на {required_amount}₽.",
await edit_or_send_message(
target_message=callback_query.message,
text=f"💳 Недостаточно средств. Пополните баланс на {required_amount}₽.",
reply_markup=builder.as_markup(),
media_path=None,
)
return
@@ -522,7 +495,16 @@ async def complete_key_renewal(tg_id, client_id, email, new_expiry_time, total_g
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
if callback_query:
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
try:
await edit_or_send_message(
target_message=callback_query.message,
text=response_message,
reply_markup=builder.as_markup(),
media_path=None,
)
except Exception as e:
logger.error(f"Ошибка редактирования сообщения в complete_key_renewal: {e}")
await callback_query.message.answer(response_message, reply_markup=builder.as_markup())
else:
await bot.send_message(tg_id, response_message, reply_markup=builder.as_markup())
+1 -1
View File
@@ -5,8 +5,8 @@ from datetime import datetime
import aiohttp
import asyncpg
from aiohttp import web
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR, USE_COUNTRY_SELECTION
from config import DATABASE_URL, PROJECT_NAME, SUB_MESSAGE, SUPERNODE, TRANSITION_DATE_STR, USE_COUNTRY_SELECTION
from database import get_key_details, get_servers
from logger import logger
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta
import asyncpg
import pytz
from aiogram import Bot, Router
from config import (
DATABASE_URL,
NOTIFICATION_TIME,
@@ -16,7 +17,6 @@ from config import (
TOTAL_GB,
TRIAL_TIME_DISABLE,
)
from database import (
add_notification,
check_notification_time,
@@ -6,8 +6,8 @@ import pytz
from aiogram import Bot, Router, types
from aiogram.exceptions import TelegramForbiddenError
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME
from config import NOTIFY_EXTRA_DAYS, NOTIFY_INACTIVE, NOTIFY_INACTIVE_TRAFFIC, SUPPORT_CHAT_URL, TRIAL_TIME
from database import (
add_notification,
check_notification_time,
+13 -4
View File
@@ -1,6 +1,7 @@
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,
@@ -10,6 +11,8 @@ from config import (
YOOMONEY_ENABLE,
)
from .utils import edit_or_send_message
router = Router()
@@ -56,15 +59,21 @@ async def handle_pay(callback_query: CallbackQuery):
builder.row(InlineKeyboardButton(text="🎟️ Активировать купон", callback_data="activate_coupon"))
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
await callback_query.message.answer(
payment_text = (
"💸 <b>Выберите удобный способ пополнения баланса:</b>\n"
"<blockquote>"
"• Быстро и безопасно\n"
"• Поддержка разных платежных систем\n"
"• Моментальное зачисление средств 🚀\n"
"</blockquote>",
reply_markup=builder.as_markup(),
"</blockquote>"
)
await edit_or_send_message(
target_message=callback_query.message,
text=payment_text,
reply_markup=builder.as_markup(),
media_path=None,
disable_web_page_preview=False,
)
+4118 -1844
View File
File diff suppressed because it is too large Load Diff
+54 -33
View File
@@ -30,6 +30,8 @@ from handlers.payments.utils import send_payment_success_notification
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
from handlers.utils import edit_or_send_message
router = Router()
@@ -102,9 +104,11 @@ async def process_callback_pay_robokassa(
await add_connection(tg_id, balance=0.0, trial=0, session=session)
logger.info(f"Created new connection for user {tg_id} with balance 0.0.")
await callback_query.message.answer(
await edit_or_send_message(
target_message=callback_query.message,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
force_text=True
)
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa)
logger.info(f"Displayed amount selection for user {tg_id}.")
@@ -119,7 +123,12 @@ async def process_amount_selection(
data = callback_query.data.split("|")
if len(data) != 3 or data[1] != "amount":
logger.error("Ошибка: callback_data не соответствует формату.")
await callback_query.message.answer("Ошибка: данные повреждены.")
await edit_or_send_message(
target_message=callback_query.message,
text="Ошибка: данные повреждены.",
reply_markup=types.InlineKeyboardMarkup(),
force_text=True
)
return
amount_str = data[2]
@@ -129,7 +138,12 @@ async def process_amount_selection(
raise ValueError("Сумма должна быть положительным числом.")
except ValueError as e:
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
await callback_query.message.answer("Некорректная сумма.")
await edit_or_send_message(
target_message=callback_query.message,
text="Некорректная сумма.",
reply_markup=types.InlineKeyboardMarkup(),
force_text=True
)
return
await state.update_data(amount=amount)
@@ -148,9 +162,11 @@ async def process_amount_selection(
]
)
await callback_query.message.answer(
await edit_or_send_message(
target_message=callback_query.message,
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
force_text=True
)
logger.info(f"Payment link sent to user {callback_query.message.chat.id}.")
@@ -228,9 +244,11 @@ async def process_custom_amount_selection(
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data="pay_robokassa"))
await callback_query.message.answer(
"Пожалуйста, введите сумму пополнения.",
await edit_or_send_message(
target_message=callback_query.message,
text="Пожалуйста, введите сумму пополнения.",
reply_markup=builder.as_markup(),
force_text=True
)
await state.set_state(
@@ -242,67 +260,70 @@ async def process_custom_amount_selection(
async def handle_custom_amount_input(message: types.Message | types.CallbackQuery, state: FSMContext = None, session: Any = None):
if isinstance(message, types.CallbackQuery):
tg_id = message.message.chat.id
target_message = message.message
else:
tg_id = message.chat.id
target_message = message
logger.info(f"User {tg_id} initiated payment through ROBOKASSA")
inv_id = 0
try:
conn = await asyncpg.connect(DATABASE_URL)
user_data = await get_temporary_data(conn, tg_id)
await conn.close()
if not user_data:
await message.answer("Данные для оплаты не найдены. Попробуйте снова.")
await edit_or_send_message(
target_message=target_message,
text="Данные для оплаты не найдены. Попробуйте снова.",
reply_markup=types.InlineKeyboardMarkup()
)
return
state_type = user_data["state"]
amount = user_data["data"].get("required_amount", 0)
if amount <= 0:
await message.answer("Недостаточная сумма для пополнения.")
await edit_or_send_message(
target_message=target_message,
text="Недостаточная сумма для пополнения.",
reply_markup=types.InlineKeyboardMarkup()
)
return
payment_url = generate_payment_link(amount, inv_id, "Пополнение баланса", tg_id)
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="💳 Оплатить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
]
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="💳 Оплатить", url=payment_url))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa"))
if state_type == "waiting_for_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для создания нового ключа. Перейдите по ссылке для оплаты:"
elif state_type == "waiting_for_renewal_payment":
message_text = f"Вы выбрали пополнение на {amount} рублей для продления ключа. Перейдите по ссылке для оплаты:"
else:
await message.answer("Некорректное состояние данных. Попробуйте снова.")
await edit_or_send_message(
target_message=target_message,
text="Некорректное состояние данных. Попробуйте снова.",
reply_markup=types.InlineKeyboardMarkup()
)
return
if isinstance(message, types.CallbackQuery):
await message.message.answer(
text=message_text,
reply_markup=confirm_keyboard,
)
else:
await message.answer(
text=message_text,
reply_markup=confirm_keyboard,
)
await edit_or_send_message(
target_message=target_message,
text=message_text,
reply_markup=builder.as_markup()
)
if isinstance(state, FSMContext):
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании платежа для пользователя {tg_id}: {e}")
error_message = "Произошла ошибка при создании платежа. Попробуйте позже."
if isinstance(message, types.CallbackQuery):
await message.message.answer(error_message)
else:
await message.answer(error_message)
await edit_or_send_message(
target_message=target_message,
text="Произошла ошибка при создании платежа. Попробуйте позже.",
reply_markup=types.InlineKeyboardMarkup()
)
+1 -14
View File
@@ -2442,7 +2442,6 @@ static const char __pyx_k_USE_NEW_PAYMENT_FLOW[] = "USE_NEW_PAYMENT_FLOW";
static const char __pyx_k_clear_temporary_data[] = "clear_temporary_data";
static const char __pyx_k_complete_key_renewal[] = "complete_key_renewal";
static const char __pyx_k_InlineKeyboardBuilder[] = "InlineKeyboardBuilder";
static const char __pyx_k_USE_COUNTRY_SELECTION[] = "USE_COUNTRY_SELECTION";
static const char __pyx_k_aiogram_utils_keyboard[] = "aiogram.utils.keyboard";
static const char __pyx_k_PAYMENT_SUCCESS_MESSAGE[] = "PAYMENT_SUCCESS_MESSAGE";
static const char __pyx_k_handlers_payments_utils[] = "handlers.payments.utils";
@@ -2495,7 +2494,6 @@ typedef struct {
PyObject *__pyx_n_s_PROFILE;
PyObject *__pyx_kp_u_RENEW;
PyObject *__pyx_n_s_RENEW_KEY;
PyObject *__pyx_n_s_USE_COUNTRY_SELECTION;
PyObject *__pyx_n_s_USE_NEW_PAYMENT_FLOW;
PyObject *__pyx_kp_u__10;
PyObject *__pyx_kp_u__11;
@@ -2683,7 +2681,6 @@ static int __pyx_m_clear(PyObject *m) {
Py_CLEAR(clear_module_state->__pyx_n_s_PROFILE);
Py_CLEAR(clear_module_state->__pyx_kp_u_RENEW);
Py_CLEAR(clear_module_state->__pyx_n_s_RENEW_KEY);
Py_CLEAR(clear_module_state->__pyx_n_s_USE_COUNTRY_SELECTION);
Py_CLEAR(clear_module_state->__pyx_n_s_USE_NEW_PAYMENT_FLOW);
Py_CLEAR(clear_module_state->__pyx_kp_u__10);
Py_CLEAR(clear_module_state->__pyx_kp_u__11);
@@ -2849,7 +2846,6 @@ static int __pyx_m_traverse(PyObject *m, visitproc visit, void *arg) {
Py_VISIT(traverse_module_state->__pyx_n_s_PROFILE);
Py_VISIT(traverse_module_state->__pyx_kp_u_RENEW);
Py_VISIT(traverse_module_state->__pyx_n_s_RENEW_KEY);
Py_VISIT(traverse_module_state->__pyx_n_s_USE_COUNTRY_SELECTION);
Py_VISIT(traverse_module_state->__pyx_n_s_USE_NEW_PAYMENT_FLOW);
Py_VISIT(traverse_module_state->__pyx_kp_u__10);
Py_VISIT(traverse_module_state->__pyx_kp_u__11);
@@ -3025,7 +3021,6 @@ static int __pyx_m_traverse(PyObject *m, visitproc visit, void *arg) {
#define __pyx_n_s_PROFILE __pyx_mstate_global->__pyx_n_s_PROFILE
#define __pyx_kp_u_RENEW __pyx_mstate_global->__pyx_kp_u_RENEW
#define __pyx_n_s_RENEW_KEY __pyx_mstate_global->__pyx_n_s_RENEW_KEY
#define __pyx_n_s_USE_COUNTRY_SELECTION __pyx_mstate_global->__pyx_n_s_USE_COUNTRY_SELECTION
#define __pyx_n_s_USE_NEW_PAYMENT_FLOW __pyx_mstate_global->__pyx_n_s_USE_NEW_PAYMENT_FLOW
#define __pyx_kp_u__10 __pyx_mstate_global->__pyx_kp_u__10
#define __pyx_kp_u__11 __pyx_mstate_global->__pyx_kp_u__11
@@ -6859,7 +6854,6 @@ static int __Pyx_CreateStringTabAndInitStrings(void) {
{&__pyx_n_s_PROFILE, __pyx_k_PROFILE, sizeof(__pyx_k_PROFILE), 0, 0, 1, 1},
{&__pyx_kp_u_RENEW, __pyx_k_RENEW, sizeof(__pyx_k_RENEW), 0, 1, 0, 0},
{&__pyx_n_s_RENEW_KEY, __pyx_k_RENEW_KEY, sizeof(__pyx_k_RENEW_KEY), 0, 0, 1, 1},
{&__pyx_n_s_USE_COUNTRY_SELECTION, __pyx_k_USE_COUNTRY_SELECTION, sizeof(__pyx_k_USE_COUNTRY_SELECTION), 0, 0, 1, 1},
{&__pyx_n_s_USE_NEW_PAYMENT_FLOW, __pyx_k_USE_NEW_PAYMENT_FLOW, sizeof(__pyx_k_USE_NEW_PAYMENT_FLOW), 0, 0, 1, 1},
{&__pyx_kp_u__10, __pyx_k__10, sizeof(__pyx_k__10), 0, 1, 0, 0},
{&__pyx_kp_u__11, __pyx_k__11, sizeof(__pyx_k__11), 0, 1, 0, 0},
@@ -7472,7 +7466,7 @@ if (!__Pyx_RefNanny) {
__Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
__Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
__pyx_t_2 = PyList_New(3); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 8, __pyx_L1_error)
__pyx_t_2 = PyList_New(2); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 8, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_2);
__Pyx_INCREF(__pyx_n_s_DATABASE_URL);
__Pyx_GIVEREF(__pyx_n_s_DATABASE_URL);
@@ -7480,9 +7474,6 @@ if (!__Pyx_RefNanny) {
__Pyx_INCREF(__pyx_n_s_USE_NEW_PAYMENT_FLOW);
__Pyx_GIVEREF(__pyx_n_s_USE_NEW_PAYMENT_FLOW);
if (__Pyx_PyList_SET_ITEM(__pyx_t_2, 1, __pyx_n_s_USE_NEW_PAYMENT_FLOW)) __PYX_ERR(0, 8, __pyx_L1_error);
__Pyx_INCREF(__pyx_n_s_USE_COUNTRY_SELECTION);
__Pyx_GIVEREF(__pyx_n_s_USE_COUNTRY_SELECTION);
if (__Pyx_PyList_SET_ITEM(__pyx_t_2, 2, __pyx_n_s_USE_COUNTRY_SELECTION)) __PYX_ERR(0, 8, __pyx_L1_error);
__pyx_t_3 = __Pyx_Import(__pyx_n_s_config, __pyx_t_2, 0); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 8, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_3);
__Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
@@ -7494,10 +7485,6 @@ if (!__Pyx_RefNanny) {
__Pyx_GOTREF(__pyx_t_2);
if (PyDict_SetItem(__pyx_d, __pyx_n_s_USE_NEW_PAYMENT_FLOW, __pyx_t_2) < 0) __PYX_ERR(0, 8, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
__pyx_t_2 = __Pyx_ImportFrom(__pyx_t_3, __pyx_n_s_USE_COUNTRY_SELECTION); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 8, __pyx_L1_error)
__Pyx_GOTREF(__pyx_t_2);
if (PyDict_SetItem(__pyx_d, __pyx_n_s_USE_COUNTRY_SELECTION, __pyx_t_2) < 0) __PYX_ERR(0, 8, __pyx_L1_error)
__Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
__Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
__pyx_t_3 = PyList_New(4); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 9, __pyx_L1_error)
File diff suppressed because it is too large Load Diff
+43 -77
View File
@@ -1,13 +1,11 @@
import os
from typing import Any
import aiofiles
import asyncpg
from aiogram import F, Router
from aiogram.enums import ParseMode
from aiogram.fsm.context import FSMContext
from aiogram.types import (
BufferedInputFile,
CallbackQuery,
InlineKeyboardButton,
InlineQuery,
@@ -16,6 +14,7 @@ from aiogram.types import (
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import (
DATABASE_URL,
INSTRUCTIONS_BUTTON,
@@ -25,7 +24,6 @@ from config import (
TRIAL_TIME,
USERNAME_BOT,
)
from database import get_balance, get_key_count, get_last_payments, get_referral_stats, get_trial
from handlers.buttons.profile import (
ADD_SUB,
@@ -39,6 +37,9 @@ from handlers.buttons.profile import (
PAYMENT,
)
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
from logger import logger
from .utils import edit_or_send_message
router = Router()
@@ -53,11 +54,11 @@ async def process_callback_view_profile(
if isinstance(callback_query_or_message, CallbackQuery):
chat_id = callback_query_or_message.message.chat.id
username = callback_query_or_message.from_user.full_name
is_callback = True
elif isinstance(callback_query_or_message, Message):
target_message = callback_query_or_message.message
else:
chat_id = callback_query_or_message.chat.id
username = callback_query_or_message.from_user.full_name
is_callback = False
target_message = callback_query_or_message
image_path = os.path.join("img", "profile.jpg")
key_count = await get_key_count(chat_id)
@@ -70,7 +71,6 @@ async def process_callback_view_profile(
trial_status = await get_trial(chat_id, conn)
profile_message = profile_message_send(username, chat_id, int(balance), key_count)
if key_count == 0:
profile_message += (
"\n<blockquote>🔧 <i>Нажмите кнопку ➕ Подписка, чтобы настроить VPN-подключение</i></blockquote>"
@@ -79,56 +79,28 @@ async def process_callback_view_profile(
profile_message += f"\n<blockquote> <i>{NEWS_MESSAGE}</i></blockquote>"
builder = InlineKeyboardBuilder()
if trial_status == 0 or key_count == 0:
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=BALANCE,
callback_data="balance",
)
)
builder.row(InlineKeyboardButton(text=BALANCE, callback_data="balance"))
builder.row(
InlineKeyboardButton(text=INVITE, callback_data="invite"),
InlineKeyboardButton(text=GIFTS, callback_data="gifts"),
)
if INSTRUCTIONS_BUTTON:
builder.row(
InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"),
)
builder.row(InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"))
if admin:
builder.row(InlineKeyboardButton(text="🔧 Администратор", callback_data="admin"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="start"))
if os.path.isfile(image_path):
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
if is_callback:
await callback_query_or_message.message.answer_photo(
photo=BufferedInputFile(image_data, filename="profile.jpg"),
caption=profile_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query_or_message.answer_photo(
photo=BufferedInputFile(image_data, filename="profile.jpg"),
caption=profile_message,
reply_markup=builder.as_markup(),
)
else:
if is_callback:
await callback_query_or_message.message.answer(
text=profile_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query_or_message.answer(
text=profile_message,
reply_markup=builder.as_markup(),
)
await edit_or_send_message(
target_message=target_message,
text=profile_message,
reply_markup=builder.as_markup(),
media_path=image_path,
disable_web_page_preview=False,
)
finally:
await conn.close()
@@ -146,9 +118,13 @@ async def balance_handler(callback_query: CallbackQuery, session: Any):
builder.row(InlineKeyboardButton(text=BALANCE_HISTORY, callback_data="balance_history"))
builder.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
await callback_query.message.answer(
f"<b>Управление вашим балансом 💰</b>\n\nВаш баланс: {balance}",
text = f"<b>Управление вашим балансом 💰</b>\n\nВаш баланс: {balance}"
await edit_or_send_message(
target_message=callback_query.message,
text=text,
reply_markup=builder.as_markup(),
media_path=None,
disable_web_page_preview=False,
)
@@ -176,7 +152,13 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any):
else:
history_text = "❌ У вас пока нет операций с балансом."
await callback_query.message.answer(history_text, reply_markup=builder.as_markup())
await edit_or_send_message(
target_message=callback_query.message,
text=history_text,
reply_markup=builder.as_markup(),
media_path=None,
disable_web_page_preview=False,
)
@router.message(F.text == "/tariffs")
@@ -186,7 +168,6 @@ async def view_tariffs_handler(callback_query: CallbackQuery):
builder.row(InlineKeyboardButton(text="👤 Личный кабинет", callback_data="profile"))
image_path = os.path.join("img", "tariffs.jpg")
tariffs_message = "<b>🚀 Доступные тарифы VPN:</b>\n\n" + "\n".join(
[
f"{months} {'месяц' if months == '1' else 'месяца' if int(months) in [2, 3, 4] else 'месяцев'}: "
@@ -196,19 +177,13 @@ async def view_tariffs_handler(callback_query: CallbackQuery):
]
)
if os.path.isfile(image_path):
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_data, filename="tariffs.jpg"),
caption=tariffs_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer(
text=tariffs_message,
reply_markup=builder.as_markup(),
)
await edit_or_send_message(
target_message=callback_query.message,
text=tariffs_message,
reply_markup=builder.as_markup(),
media_path=image_path,
disable_web_page_preview=False,
)
@router.callback_query(F.data == "invite")
@@ -216,30 +191,21 @@ async def invite_handler(callback_query: CallbackQuery):
chat_id = callback_query.message.chat.id
referral_link = get_referral_link(chat_id)
referral_stats = await get_referral_stats(chat_id)
invite_text = f"\nПриглашаю тебя пользоваться действительно быстрым VPN вместе:\n\n{referral_link}"
invite_message = invite_message_send(referral_link, referral_stats)
image_path = os.path.join("img", "pic_invite.jpg")
builder = InlineKeyboardBuilder()
# builder.button(text="📢 Поделиться", switch_inline_query=invite_text)
builder.button(text="👥 Пригласить друга", switch_inline_query="invite ")
builder.button(text="👤 Личный кабинет", callback_data="profile")
builder.adjust(1)
if os.path.isfile(image_path):
async with aiofiles.open(image_path, "rb") as image_file:
image_data = await image_file.read()
await callback_query.message.answer_photo(
photo=BufferedInputFile(image_data, filename="pic_invite.jpg"),
caption=invite_message,
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer(
text=invite_message,
reply_markup=builder.as_markup(),
)
await edit_or_send_message(
target_message=callback_query.message,
text=invite_message,
reply_markup=builder.as_markup(),
media_path=image_path,
disable_web_page_preview=False,
)
@router.inline_query(F.query.in_(["referral", "ref", "invite"]))
+35 -33
View File
@@ -1,17 +1,17 @@
import os
from typing import Any
import aiofiles
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.types import (
BufferedInputFile,
CallbackQuery,
InlineKeyboardButton,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import (
CAPTCHA_ENABLE,
CHANNEL_EXISTS,
@@ -21,8 +21,6 @@ from config import (
DONATIONS_ENABLE,
SUPPORT_CHAT_URL,
)
from bot import bot
from database import (
add_connection,
add_referral,
@@ -36,6 +34,8 @@ from handlers.keys.key_management import create_key
from handlers.texts import WELCOME_TEXT, get_about_vpn
from logger import logger
from .utils import edit_or_send_message
router = Router()
@@ -59,7 +59,11 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
if CAPTCHA_ENABLE and captcha:
captcha_data = await generate_captcha(message, state)
await message.answer(text=captcha_data["text"], reply_markup=captcha_data["markup"])
await edit_or_send_message(
target_message=message,
text=captcha_data["text"],
reply_markup=captcha_data["markup"],
)
return
if CHANNEL_EXISTS and CHANNEL_REQUIRED:
@@ -70,8 +74,9 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await state.update_data(original_text=original_text)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
await message.answer(
f"Для использования бота, пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}",
await edit_or_send_message(
target_message=message,
text=f"Для использования бота, пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}",
reply_markup=builder.as_markup(),
)
return
@@ -84,8 +89,10 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
await state.update_data(start_text=message.text)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
await message.answer(
f"Пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}", reply_markup=builder.as_markup()
await edit_or_send_message(
target_message=message,
text=f"Пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}",
reply_markup=builder.as_markup(),
)
return
@@ -287,7 +294,7 @@ async def check_subscription_callback(callback_query: CallbackQuery, state: FSMC
await callback_query.answer("Вы еще не подписаны на канал!", show_alert=True)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="✅ Я подписался", callback_data="check_subscription"))
await callback_query.message.answer(
await callback_query.message.edit_text(
f"Для использования бота, пожалуйста, подпишитесь на наш канал: {CHANNEL_URL}",
reply_markup=builder.as_markup(),
)
@@ -305,7 +312,8 @@ async def check_subscription_callback(callback_query: CallbackQuery, state: FSMC
async def show_start_menu(message: Message, admin: bool, session: Any):
"""Функция для отображения стандартного меню"""
"""Функция для отображения стандартного меню через редактирование сообщения.
Если редактирование не удалось, отправляем новое сообщение."""
logger.info(f"Показываю главное меню для пользователя {message.chat.id}")
image_path = os.path.join("img", "pic.jpg")
@@ -334,35 +342,29 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
builder.row(InlineKeyboardButton(text="🌐 О VPN", callback_data="about_vpn"))
if os.path.isfile(image_path):
async with aiofiles.open(image_path, "rb") as image_from_buffer:
image_data = await image_from_buffer.read()
await message.answer_photo(
photo=BufferedInputFile(image_data, filename="pic.jpg"),
caption=WELCOME_TEXT,
reply_markup=builder.as_markup(),
)
else:
await message.answer(
text=WELCOME_TEXT,
reply_markup=builder.as_markup(),
)
await edit_or_send_message(
target_message=message,
text=WELCOME_TEXT,
reply_markup=builder.as_markup(),
media_path=image_path,
)
@router.callback_query(F.data == "about_vpn")
async def handle_about_vpn(callback_query: CallbackQuery):
builder = InlineKeyboardBuilder()
if DONATIONS_ENABLE:
builder.row(InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate"))
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
builder.row(InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL))
if CHANNEL_EXISTS:
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
)
builder.row(InlineKeyboardButton(text="📢 Канал", url=CHANNEL_URL))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="start"))
text = get_about_vpn("3.2.3-minor")
await callback_query.message.answer(get_about_vpn("3.2.3-minor"), reply_markup=builder.as_markup())
await edit_or_send_message(
target_message=callback_query.message,
text=text,
reply_markup=builder.as_markup(),
media_path=None,
force_text=False,
)
+65 -1
View File
@@ -1,13 +1,16 @@
import json
import os
import re
import secrets
import string
import aiofiles
import aiohttp
import asyncpg
from config import DATABASE_URL
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message
from bot import bot
from config import DATABASE_URL
from database import get_all_keys, get_servers
from logger import logger
@@ -142,3 +145,64 @@ def format_time_until_deletion(seconds: int) -> str:
parts.append(f"{minutes} минут")
return " и ".join(parts) if parts else "менее минуты"
async def edit_or_send_message(
target_message: Message,
text: str,
reply_markup: InlineKeyboardMarkup,
media_path: str = None,
disable_web_page_preview: bool = False,
force_text: bool = False,
):
"""
Универсальная функция для редактирования исходного сообщения target_message.
- Если media_path указан и существует, считается, что сообщение содержит фото, и используется редактирование медиа
(замена фото и подписи) через edit_media. Если редактирование не удаётся, отправляется новое сообщение с фото.
- Если media_path не указан:
- Если force_text=False и target_message уже имеет caption, пытаемся отредактировать подпись (edit_caption).
- Иначе (или если редактирование caption не удалось) редактируем текст (edit_text).
В случае неудачи fallback отправка нового сообщения.
"""
if media_path and os.path.isfile(media_path):
async with aiofiles.open(media_path, "rb") as f:
image_data = await f.read()
media = InputMediaPhoto(
media=BufferedInputFile(image_data, filename=os.path.basename(media_path)),
caption=text,
)
try:
await target_message.edit_media(media=media, reply_markup=reply_markup)
return
except Exception as e:
await target_message.answer_photo(
photo=BufferedInputFile(image_data, filename=os.path.basename(media_path)),
caption=text,
reply_markup=reply_markup,
disable_web_page_preview=disable_web_page_preview,
)
return
else:
if not force_text and target_message.caption is not None:
try:
await target_message.edit_caption(caption=text, reply_markup=reply_markup)
return
except Exception as e:
logger.error("Ошибка редактирования подписи: %s", e)
try:
await target_message.edit_text(
text=text,
reply_markup=reply_markup,
disable_web_page_preview=disable_web_page_preview,
)
return
except Exception as e:
logger.error("Ошибка редактирования текста: %s", e)
await target_message.answer(
text=text,
reply_markup=reply_markup,
disable_web_page_preview=disable_web_page_preview,
)
+1 -1
View File
@@ -3,8 +3,8 @@ from datetime import datetime, timezone
from aiogram.filters.callback_data import CallbackData
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import RENEWAL_PRICES
from config import RENEWAL_PRICES
from keyboards.admin.panel_kb import build_admin_back_btn
+1 -1
View File
File diff suppressed because one or more lines are too long
-2
View File
@@ -3,7 +3,6 @@ from typing import Any
from aiogram import Dispatcher
from .admin import AdminMiddleware
from .delete import DeleteMessageMiddleware
from .loggings import LoggingMiddleware
from .session import SessionMiddleware
from .throttling import ThrottlingMiddleware
@@ -14,7 +13,6 @@ def register_middleware(dispatcher: Dispatcher) -> None:
middlewares = [
AdminMiddleware(),
SessionMiddleware(),
DeleteMessageMiddleware(),
LoggingMiddleware(),
ThrottlingMiddleware(),
UserMiddleware(),
+1
View File
@@ -3,6 +3,7 @@ from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import ADMIN_ID
-52
View File
@@ -1,52 +0,0 @@
from collections.abc import Awaitable, Callable
from typing import Any
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from keyboards.admin.coupons_kb import AdminCouponDeleteCallback
from keyboards.admin.panel_kb import AdminPanelCallback
from keyboards.admin.sender_kb import AdminSenderCallback
from keyboards.admin.servers_kb import AdminServerEditorCallback
from keyboards.admin.users_kb import AdminUserEditorCallback, AdminUserKeyEditorCallback
from logger import logger
pass_callbacks = [
AdminPanelCallback,
AdminCouponDeleteCallback,
AdminSenderCallback,
AdminServerEditorCallback,
AdminUserEditorCallback,
AdminUserKeyEditorCallback,
]
class DeleteMessageMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: dict[str, Any],
) -> Any:
if isinstance(event, Message):
if not event.text or not event.text.startswith("/start"):
try:
await event.bot.delete_message(event.chat.id, event.message_id - 1)
except Exception as e:
logger.error(e)
await event.delete()
if isinstance(event, CallbackQuery):
await event.answer()
if not await self._check_callbacks(event):
await event.message.delete()
return await handler(event, data)
@staticmethod
async def _check_callbacks(event: CallbackQuery) -> bool:
for callback in pass_callbacks:
if await callback.filter()(event):
return True
return False
+1
View File
@@ -4,6 +4,7 @@ from typing import Any
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
+11 -46
View File
@@ -2,56 +2,16 @@ import asyncio
import re
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 database import check_unique_server_name, create_server, get_servers
from config import ADMIN_ID, DATABASE_URL, PING_TIME
from database import get_servers
from keyboards.admin.servers_kb import AdminServerEditorCallback
from logger import logger
try:
from config import CLUSTERS
except ImportError:
CLUSTERS = None
logger.info("Переменная CLUSTERS не найдена в конфигурации. Добавьте сервера через админ-панель в боте!")
async def sync_servers_with_db():
"""
Синхронизирует сервера из конфигурации CLUSTERS с базой данных.
"""
if CLUSTERS is None:
logger.info("Конфигурация CLUSTERS не найдена. Синхронизация не будет выполнена.")
return
try:
conn = await asyncpg.connect(DATABASE_URL)
for cluster_name, servers in CLUSTERS.items():
for _server_key, server_info in servers.items():
exists = await check_unique_server_name(server_info["name"], conn, cluster_name)
if not exists:
await create_server(
cluster_name=cluster_name,
server_name=server_info["name"],
api_url=server_info["API_URL"],
subscription_url=server_info["SUBSCRIPTION"],
inbound_id=server_info["INBOUND_ID"],
session=conn,
)
logger.info("✅ Синхронизация серверов завершена.")
except Exception as e:
logger.error(f"Ошибка при синхронизации серверов: {e}")
finally:
if "conn" in locals():
await conn.close()
last_ping_times = {}
last_notification_times = {}
PING_SEMAPHORE = asyncio.Semaphore(3)
@@ -89,7 +49,12 @@ async def notify_admin(server_name: str):
return
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Управление сервером", callback_data=f"manage_server|{server_name}"))
builder.row(
InlineKeyboardButton(
text="Управление сервером",
callback_data=AdminServerEditorCallback(action="servers_manage", data=server_name).pack(),
)
)
for admin_id in ADMIN_ID:
await bot.send_message(
@@ -116,7 +81,7 @@ async def check_servers():
tasks = []
server_info_list = []
for cluster_name, cluster_servers in servers.items():
for _, cluster_servers in servers.items():
for server in cluster_servers:
original_api_url = server["api_url"]
server_name = server["server_name"]
@@ -141,7 +106,7 @@ async def check_servers():
last_ping_times[server_name] = current_time
online_servers = [name for name, _ in server_info_list if name not in offline_servers]
logger.info(f"Проверка серверов завершена. Онлайн: {len(online_servers)}, Оффлайн: {len(offline_servers)}")
logger.info(f"Проверка серверов завершена. Доступно: {len(online_servers)}, Недоступно: {len(offline_servers)}")
if offline_servers:
logger.warning(f"🚨 Не отвечает {len(offline_servers)} серверов: {', '.join(offline_servers)}")