Merge pull request #78 from izzzzzi/main

Refactoring and fix
This commit is contained in:
Vladislav Lisitsyn
2024-11-09 23:59:29 +03:00
committed by GitHub
26 changed files with 2476 additions and 1403 deletions
+50 -13
View File
@@ -1,14 +1,51 @@
/venv
/__pycache__
/vpn_users.db
/config.py
/database.db
/bot_old.py
/bot_old_2.py
/database.db
/backup_pg.sh
/config copy.py
/docker-compose.yml
__pycache__
/handlers/texts.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
# Virtual environments
venv/
env/
.env/
.venv/
# IDE specific files
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
# Logs and databases
*.log
*.sqlite3
*.db
# Sensitive configuration files
config.py
config.ini
.env
# Backup files
*.bak
*.swp
*~
# Specific project files
vpn_users.db
database.db
bot_old.py
bot_old_2.py
backup_pg.sh
docker-compose.yml
config copy.py
handlers/texts.py
# Miscellaneous
.DS_Store
Thumbs.db
+27 -24
View File
@@ -6,32 +6,34 @@ from config import SERVERS
session = None
async def login_with_credentials(server_id: str, username: str, password: str):
global session
session = aiohttp.ClientSession()
api_url = SERVERS[server_id]['API_URL']
api_url = SERVERS[server_id]["API_URL"]
auth_url = f"{api_url}/login/"
data = {
"username": username,
"password": password
}
data = {"username": username, "password": password}
async with session.post(auth_url, json=data) as response:
if response.status == 200:
session.cookie_jar.update_cookies(response.cookies)
return session
else:
raise Exception(f"Ошибка авторизации: {response.status}, {await response.text()}")
raise Exception(
f"Ошибка авторизации: {response.status}, {await response.text()}"
)
async def get_clients(session, server_id):
api_url = SERVERS[server_id]['API_URL']
async with session.get(f'{api_url}/panel/api/inbounds/list/') as response:
api_url = SERVERS[server_id]["API_URL"]
async with session.get(f"{api_url}/panel/api/inbounds/list/") as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Ошибка при получении клиентов: {response.status}, {await response.text()}")
raise Exception(
f"Ошибка при получении клиентов: {response.status}, {await response.text()}"
)
async def link(session, server_id: str, client_id: str, email: str):
@@ -42,29 +44,30 @@ async def link(session, server_id: str, client_id: str, email: str):
:param email: str - электронная почта клиента
:return: str - ссылка для подключения
"""
response = await get_clients(session, server_id)
if 'obj' not in response or len(response['obj']) == 0:
response = await get_clients(session, server_id)
if "obj" not in response or len(response["obj"]) == 0:
raise Exception("Не удалось получить данные клиентов.")
inbounds = response['obj'][0]
settings = json.loads(inbounds['settings'])
stream_settings = json.loads(inbounds['streamSettings'])
tcp = stream_settings.get('network', 'tcp')
reality = stream_settings.get('security', 'reality')
flow = stream_settings.get('flow', 'xtls-rprx-vision')
inbounds = response["obj"][0]
settings = json.loads(inbounds["settings"])
stream_settings = json.loads(inbounds["streamSettings"])
tcp = stream_settings.get("network", "tcp")
reality = stream_settings.get("security", "reality")
flow = stream_settings.get("flow", "xtls-rprx-vision")
val = (
f"vless://{client_id}@{SERVERS[server_id]['DOMEN']}?type={tcp}&security={reality}&pbk={SERVERS[server_id]['PBK']}"
f"&fp=chrome&sni={SERVERS[server_id]['SNI']}&sid={SERVERS[server_id]['SID']}=%2F&flow={flow}#{SERVERS[server_id]['PREFIX']}-{email}"
)
return val
async def link_subscription(email, server_id):
server = SERVERS.get(server_id)
if server:
subscription_url = f"{server['SUBSCRIPTION']}/{email}"
return subscription_url
else:
raise ValueError(f"Server '{server_id}' not found in configuration.")
raise ValueError(f"Server '{server_id}' not found in configuration.")
+30 -15
View File
@@ -1,9 +1,9 @@
import logging
import os
import subprocess
from datetime import datetime
from aiogram.types import BufferedInputFile
from loguru import logger
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
@@ -17,33 +17,48 @@ async def backup_database():
DATE = datetime.now().strftime("%Y-%m-%d-%H%M%S")
BACKUP_FILE = f"{BACKUP_DIR}/{DB_NAME}-backup-{DATE}.sql"
os.environ['PGPASSWORD'] = DB_PASSWORD
os.environ["PGPASSWORD"] = DB_PASSWORD
try:
subprocess.run(
['pg_dump', '-U', USER, '-h', HOST, '-F', 'c', '-f', BACKUP_FILE, DB_NAME],
check=True
["pg_dump", "-U", USER, "-h", HOST, "-F", "c", "-f", BACKUP_FILE, DB_NAME],
check=True,
)
logging.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
logger.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при создании бэкапа базы данных: {e}")
logger.error(f"Ошибка при создании бэкапа базы данных: {e}")
return
try:
with open(BACKUP_FILE, 'rb') as backup_file:
backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(BACKUP_FILE))
with open(BACKUP_FILE, "rb") as backup_file:
backup_input_file = BufferedInputFile(
backup_file.read(), filename=os.path.basename(BACKUP_FILE)
)
await bot.send_document(ADMIN_ID, backup_input_file)
logging.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
except Exception as e:
logging.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
try:
subprocess.run(
['find', BACKUP_DIR, '-type', 'f', '-name', '*.sql', '-mtime', '+7', '-exec', 'rm', '{}', ';'],
check=True
[
"find",
BACKUP_DIR,
"-type",
"f",
"-name",
"*.sql",
"-mtime",
"+7",
"-exec",
"rm",
"{}",
";",
],
check=True,
)
logging.info("Старые бэкапы удалены.")
logger.info("Старые бэкапы удалены.")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при удалении старых бэкапов: {e}")
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
del os.environ['PGPASSWORD']
del os.environ["PGPASSWORD"]
+13 -4
View File
@@ -1,7 +1,7 @@
from aiogram import Bot, Dispatcher, Router
from aiogram.fsm.storage.memory import MemoryStorage
from config import API_TOKEN
from config import API_TOKEN, FREEKASSA_ENABLE, YOOKASSA_ENABLE
bot = Bot(token=API_TOKEN)
storage = MemoryStorage()
@@ -11,7 +11,9 @@ router = Router()
from handlers import commands, notifications, profile, start
from handlers.admin import admin, admin_panel, user_editor
from handlers.keys import key_management, keys
from handlers.payment import freekassa, pay
from handlers.payment import freekassa_pay, yookassa_pay
from middlewares.admin import AdminMiddleware
from middlewares.logging import UserActivityMiddleware
dp.include_router(admin.router)
dp.include_router(admin_panel.router)
@@ -21,6 +23,13 @@ dp.include_router(start.router)
dp.include_router(profile.router)
dp.include_router(keys.router)
dp.include_router(key_management.router)
dp.include_router(pay.router)
dp.include_router(freekassa.router)
if YOOKASSA_ENABLE:
dp.include_router(yookassa_pay.router)
if FREEKASSA_ENABLE:
dp.include_router(freekassa_pay.router)
dp.include_router(notifications.router)
dp.message.middleware(AdminMiddleware())
dp.callback_query.middleware(AdminMiddleware())
dp.message.middleware(UserActivityMiddleware())
dp.callback_query.middleware(UserActivityMiddleware())
+128 -96
View File
@@ -1,14 +1,27 @@
import json
from loguru import logger
from config import SERVERS
async def add_client(session, server_id: str, client_id: str, email: str, tg_id: str, limit_ip: int, total_gb: int, expiry_time: int, enable: bool, flow: str):
api_url = SERVERS[server_id]['API_URL']
url = f'{api_url}/panel/api/inbounds/addClient'
async def add_client(
session,
server_id: str,
client_id: str,
email: str,
tg_id: str,
limit_ip: int,
total_gb: int,
expiry_time: int,
enable: bool,
flow: str,
):
api_url = SERVERS[server_id]["API_URL"]
url = f"{api_url}/panel/api/inbounds/addClient"
email = email.lower()
client_data = {
"id": client_id,
"alterId": 0,
@@ -21,53 +34,58 @@ async def add_client(session, server_id: str, client_id: str, email: str, tg_id:
"subId": email,
"flow": flow,
}
settings = json.dumps({"clients": [client_data]})
data = {
"id": 1,
"settings": settings
}
data = {"id": 1, "settings": settings}
headers = {
'Content-Type': 'application/json',
"Content-Type": "application/json",
}
async with session.post(url, json=data, headers=headers) as response:
print(f"Запрос на добавление клиента: {data}")
print(f"Статус ответа: {response.status}")
logger.info(f"Запрос на добавление клиента: {data}")
logger.info(f"Статус ответа: {response.status}")
response_text = await response.text()
print(f"Ответ от сервера: {response_text}")
logger.info(f"Ответ от сервера: {response_text}")
if response.status == 200:
print(f"Клиент добавлен: email={email}")
logger.info(f"Клиент добавлен: email={email}")
return await response.json()
else:
print(f"Ошибка при добавлении клиента: {response.status}, {response_text}")
logger.error(
f"Ошибка при добавлении клиента: {response.status}, {response_text}"
)
return None
async def extend_client_key(session, server_id: str, tg_id, client_id, email: str, new_expiry_time: int) -> bool:
api_url = SERVERS[server_id]['API_URL']
async with session.get(f"{api_url}/panel/api/inbounds/getClientTraffics/{email}") as response:
print(f"GET {response.url} Status: {response.status}")
async def extend_client_key(
session, server_id: str, tg_id, client_id, email: str, new_expiry_time: int
) -> bool:
api_url = SERVERS[server_id]["API_URL"]
async with session.get(
f"{api_url}/panel/api/inbounds/getClientTraffics/{email}"
) as response:
logger.info(f"GET {response.url} Status: {response.status}")
response_text = await response.text()
print(f"GET Response: {response_text}")
logger.info(f"GET Response: {response_text}")
if response.status != 200:
print(f"Ошибка при получении данных клиента: {response.status} - {response_text}")
logger.error(
f"Ошибка при получении данных клиента: {response.status} - {response_text}"
)
return False
client_data = (await response.json()).get("obj", {})
print(client_data)
logger.info(client_data)
if not client_data:
print("Не удалось получить данные клиента.")
logger.error("Не удалось получить данные клиента.")
return False
current_expiry_time = client_data.get('expiryTime', 0)
current_expiry_time = client_data.get("expiryTime", 0)
if current_expiry_time == 0:
current_expiry_time = new_expiry_time
@@ -75,7 +93,60 @@ async def extend_client_key(session, server_id: str, tg_id, client_id, email: st
payload = {
"id": 1,
"settings": json.dumps({
"settings": json.dumps(
{
"clients": [
{
"id": client_id,
"alterId": 0,
"email": email.lower(),
"limitIp": 2,
"totalGB": 0,
"expiryTime": updated_expiry_time,
"enable": True,
"tgId": tg_id,
"subId": email,
"flow": "xtls-rprx-vision",
}
]
}
),
}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
try:
async with session.post(
f"{api_url}/panel/api/inbounds/updateClient/{client_id}",
json=payload,
headers=headers,
) as response:
logger.info(f"POST {response.url} Status: {response.status}")
logger.info(f"POST Request Data: {json.dumps(payload, indent=2)}")
response_text = await response.text()
logger.info(f"POST Response: {response_text}")
if response.status == 200:
return True
else:
logger.error(
f"Ошибка при продлении ключа: {response.status} - {response_text}"
)
return False
except Exception as e:
logger.error(f"Ошибка запроса: {e}")
return False
async def extend_client_key_admin(
session, server_id: str, tg_id, client_id: str, email: str, new_expiry_time: int
) -> bool:
api_url = SERVERS[server_id]["API_URL"]
payload = {
"id": 1,
"settings": json.dumps(
{
"clients": [
{
"id": client_id,
@@ -83,95 +154,56 @@ async def extend_client_key(session, server_id: str, tg_id, client_id, email: st
"email": email.lower(),
"limitIp": 2,
"totalGB": 0,
"expiryTime": updated_expiry_time,
"expiryTime": new_expiry_time,
"enable": True,
"tgId": tg_id,
"subId": email,
"flow": "xtls-rprx-vision"
"flow": "xtls-rprx-vision",
}
]
})
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
try:
async with session.post(f"{api_url}/panel/api/inbounds/updateClient/{client_id}", json=payload, headers=headers) as response:
print(f"POST {response.url} Status: {response.status}")
print(f"POST Request Data: {json.dumps(payload, indent=2)}")
response_text = await response.text()
print(f"POST Response: {response_text}")
if response.status == 200:
return True
else:
print(f"Ошибка при продлении ключа: {response.status} - {response_text}")
return False
except Exception as e:
print(f"Ошибка запроса: {e}")
return False
}
),
}
headers = {"Content-Type": "application/json", "Accept": "application/json"}
async def extend_client_key_admin(session, server_id: str, tg_id, client_id: str, email: str, new_expiry_time: int) -> bool:
api_url = SERVERS[server_id]['API_URL']
payload = {
"id": 1,
"settings": json.dumps({
"clients": [
{
"id": client_id,
"alterId": 0,
"email": email.lower(),
"limitIp": 2,
"totalGB": 0,
"expiryTime": new_expiry_time,
"enable": True,
"tgId": tg_id,
"subId": email,
"flow": "xtls-rprx-vision"
}
]
})
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
try:
async with session.post(f"{api_url}/panel/api/inbounds/updateClient/{client_id}", json=payload, headers=headers) as response:
print(f"POST {response.url} Status: {response.status}")
print(f"POST Request Data: {json.dumps(payload, indent=2)}")
async with session.post(
f"{api_url}/panel/api/inbounds/updateClient/{client_id}",
json=payload,
headers=headers,
) as response:
logger.info(f"POST {response.url} Status: {response.status}")
logger.info(f"POST Request Data: {json.dumps(payload, indent=2)}")
response_text = await response.text()
print(f"POST Response: {response_text}")
logger.info(f"POST Response: {response_text}")
if response.status == 200:
return True
else:
print(f"Ошибка при продлении ключа: {response.status} - {response_text}")
logger.error(
f"Ошибка при продлении ключа: {response.status} - {response_text}"
)
return False
except Exception as e:
print(f"Ошибка запроса: {e}")
logger.error(f"Ошибка запроса: {e}")
return False
async def delete_client(session, server_id: str, client_id: str) -> bool:
api_url = SERVERS[server_id]['API_URL']
api_url = SERVERS[server_id]["API_URL"]
url = f"{api_url}/panel/api/inbounds/1/delClient/{client_id}"
headers = {
'Accept': 'application/json'
}
headers = {"Accept": "application/json"}
try:
async with session.post(url, headers=headers) as response:
if response.status == 200:
return True
else:
print(f"Ошибка при удалении клиента: {response.status} - {await response.text()}")
logger.error(
f"Ошибка при удалении клиента: {response.status} - {await response.text()}"
)
return False
except Exception as e:
print(f"Ошибка запроса: {e}")
logger.error(f"Ошибка запроса: {e}")
return False
+152 -60
View File
@@ -7,16 +7,19 @@ from config import DATABASE_URL
async def init_db():
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS connections (
tg_id BIGINT PRIMARY KEY NOT NULL,
balance REAL NOT NULL DEFAULT 0.0,
trial INTEGER NOT NULL DEFAULT 0
)
''')
await conn.execute('''
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS keys (
tg_id BIGINT NOT NULL,
client_id TEXT NOT NULL,
@@ -28,182 +31,256 @@ async def init_db():
notified BOOLEAN NOT NULL DEFAULT FALSE, -- новое поле для статуса уведомления
PRIMARY KEY (tg_id, client_id)
)
''')
await conn.execute('''
"""
)
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS referrals (
referred_tg_id BIGINT PRIMARY KEY NOT NULL, -- ID приглашенного пользователя
referrer_tg_id BIGINT NOT NULL, -- ID пригласившего пользователя
reward_issued BOOLEAN DEFAULT FALSE -- Был ли начислен бонус
)
''')
"""
)
try:
await conn.execute('''
await conn.execute(
"""
ALTER TABLE keys
ADD COLUMN server_id TEXT NOT NULL DEFAULT 'server1'
''')
"""
)
except asyncpg.exceptions.DuplicateColumnError:
pass
try:
await conn.execute('''
await conn.execute(
"""
ALTER TABLE keys
ADD COLUMN notified BOOLEAN NOT NULL DEFAULT FALSE
''')
"""
)
except asyncpg.exceptions.DuplicateColumnError:
pass
try:
await conn.execute('''
await conn.execute(
"""
ALTER TABLE keys
ADD COLUMN notified_24h BOOLEAN NOT NULL DEFAULT FALSE
''')
"""
)
except asyncpg.exceptions.DuplicateColumnError:
pass
await conn.close()
async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
INSERT INTO connections (tg_id, balance, trial)
VALUES ($1, $2, $3)
''', tg_id, balance, trial)
""",
tg_id,
balance,
trial,
)
await conn.close()
async def check_connection_exists(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
exists = await conn.fetchval('''
exists = await conn.fetchval(
"""
SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1)
''', tg_id)
""",
tg_id,
)
await conn.close()
return exists
async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str):
async def store_key(
tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str
):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
''', tg_id, client_id, email, int(datetime.utcnow().timestamp() * 1000), expiry_time, key, server_id)
""",
tg_id,
client_id,
email,
int(datetime.utcnow().timestamp() * 1000),
expiry_time,
key,
server_id,
)
await conn.close()
async def get_keys(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
records = await conn.fetch('''
records = await conn.fetch(
"""
SELECT client_id, email, created_at, key
FROM keys
WHERE tg_id = $1
''', tg_id)
""",
tg_id,
)
await conn.close()
return records
async def get_keys_by_server(tg_id: int, server_id: str):
conn = await asyncpg.connect(DATABASE_URL)
records = await conn.fetch('''
records = await conn.fetch(
"""
SELECT client_id, email, created_at, key
FROM keys
WHERE tg_id = $1 AND server_id = $2
''', tg_id, server_id)
""",
tg_id,
server_id,
)
await conn.close()
return records
async def has_active_key(tg_id: int) -> bool:
conn = await asyncpg.connect(DATABASE_URL)
count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id)
await conn.close()
return count > 0
async def get_balance(tg_id: int) -> float:
conn = await asyncpg.connect(DATABASE_URL)
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
balance = await conn.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
await conn.close()
return balance if balance is not None else 0.0
async def update_balance(tg_id: int, amount: float):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
UPDATE connections
SET balance = balance + $1
WHERE tg_id = $2
''', amount, tg_id)
""",
amount,
tg_id,
)
await handle_referral_on_balance_update(tg_id, amount)
await conn.close()
async def get_trial(tg_id: int) -> int:
conn = await asyncpg.connect(DATABASE_URL)
trial = await conn.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id)
await conn.close()
return trial if trial is not None else 0
async def get_key_count(tg_id: int) -> int:
conn = await asyncpg.connect(DATABASE_URL)
count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE tg_id = $1', tg_id)
count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id)
await conn.close()
return count if count is not None else 0
async def get_all_users(conn):
return await conn.fetch('SELECT tg_id FROM connections')
return await conn.fetch("SELECT tg_id FROM connections")
async def add_referral(referred_tg_id: int, referrer_tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
INSERT INTO referrals (referred_tg_id, referrer_tg_id)
VALUES ($1, $2)
''', referred_tg_id, referrer_tg_id)
""",
referred_tg_id,
referrer_tg_id,
)
await conn.close()
async def handle_referral_on_balance_update(tg_id: int, amount: float):
conn = await asyncpg.connect(DATABASE_URL)
referral = await conn.fetchrow('''
referral = await conn.fetchrow(
"""
SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1
''', tg_id)
""",
tg_id,
)
if referral:
referrer_tg_id = referral['referrer_tg_id']
bonus = amount * 0.25
referrer_tg_id = referral["referrer_tg_id"]
bonus = amount * 0.25
if bonus < 0:
bonus = 0
await update_balance(referrer_tg_id, bonus)
await conn.execute('''
await conn.execute(
"""
UPDATE referrals SET reward_issued = TRUE
WHERE referrer_tg_id = $1 AND referred_tg_id = $2
''', referrer_tg_id, tg_id)
""",
referrer_tg_id,
tg_id,
)
await conn.close()
async def get_referral_stats(referrer_tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
total_referrals = await conn.fetchval('''
total_referrals = await conn.fetchval(
"""
SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1
''', referrer_tg_id)
""",
referrer_tg_id,
)
active_referrals = await conn.fetchval('''
active_referrals = await conn.fetchval(
"""
SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE
''', referrer_tg_id)
""",
referrer_tg_id,
)
await conn.close()
return {
'total_referrals': total_referrals,
'active_referrals': active_referrals
}
return {"total_referrals": total_referrals, "active_referrals": active_referrals}
async def update_key_expiry(client_id: str, new_expiry_time: int):
"""
Обновление времени истечения ключа на новое значение.
"""
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
UPDATE keys
SET expiry_time = $1, notified = FALSE, notified_24h = FALSE
WHERE client_id = $2
''', new_expiry_time, client_id)
""",
new_expiry_time,
client_id,
)
await conn.close()
@@ -212,36 +289,51 @@ async def delete_key(client_id: str):
Удаление ключа из базы данных.
"""
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
DELETE FROM keys
WHERE client_id = $1
''', client_id)
""",
client_id,
)
await conn.close()
async def add_balance_to_client(client_id: str, amount: float):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
UPDATE connections
SET balance = balance + $1
WHERE tg_id = $2
''', amount, client_id)
""",
amount,
client_id,
)
await conn.close()
async def get_client_id_by_email(email: str):
"""
Получение client_id по email.
"""
conn = await asyncpg.connect(DATABASE_URL)
client_id = await conn.fetchval('''
client_id = await conn.fetchval(
"""
SELECT client_id FROM keys WHERE email = $1
''', email)
""",
email,
)
await conn.close()
return client_id
async def get_tg_id_by_client_id(client_id: str):
conn = await asyncpg.connect(DATABASE_URL)
try:
result = await conn.fetchrow('SELECT tg_id FROM keys WHERE client_id = $1', client_id)
return result['tg_id'] if result else None
result = await conn.fetchrow(
"SELECT tg_id FROM keys WHERE client_id = $1", client_id
)
return result["tg_id"] if result else None
finally:
await conn.close()
await conn.close()
+94 -70
View File
@@ -1,86 +1,110 @@
from datetime import datetime
import asyncpg
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.filters import Command
from loguru import logger
from auth import login_with_credentials
from client import extend_client_key_admin
from config import ADMIN_ID, ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import (add_balance_to_client, check_connection_exists,
get_client_id_by_email, get_tg_id_by_client_id,
update_key_expiry)
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL
from database import (
add_balance_to_client,
check_connection_exists,
get_client_id_by_email,
get_tg_id_by_client_id,
update_key_expiry,
)
router = Router()
@router.message(Command('add_balance'))
async def cmd_add_balance(message: types.Message):
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
try:
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"Клиент с ID {client_id} не найден.")
return
await add_balance_to_client(int(client_id), amount)
await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.")
except ValueError:
await message.reply("Пожалуйста, используйте формат: /add_balance <client_id> <amount>")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
@router.message(Command('update_key_expiry'))
async def cmd_update_key_expiry(message: types.Message):
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
try:
parts = message.text.split(maxsplit=2)
if len(parts) != 3:
await message.reply("Пожалуйста, используйте формат: /update_key_expiry <email> <expiry_time(YYYY-MM-DD HH:MM:SS)>")
return
_, email, expiry_time_str = parts
expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
return
await update_key_expiry(client_id, expiry_time)
conn = await asyncpg.connect(DATABASE_URL)
@router.message(Command("add_balance"))
async def cmd_add_balance(message: types.Message, is_admin: bool):
if is_admin:
try:
record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id)
if not record:
await message.reply("Клиент не найден в базе данных.")
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"Клиент с ID {client_id} не найден.")
return
server_id = record['server_id']
tg_id = await get_tg_id_by_client_id(client_id)
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
print(f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}")
await add_balance_to_client(int(client_id), amount)
await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.")
except ValueError:
await message.reply(
"Пожалуйста, используйте формат: /add_balance <client_id> <amount>"
)
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
success = await extend_client_key_admin(session, server_id, tg_id, client_id, email, expiry_time)
print(f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}")
if success:
await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено и синхронизировано с панелью.")
else:
await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью.")
finally:
await conn.close()
except ValueError:
await message.reply("Пожалуйста, используйте формат: /update_key_expiry <email> <expiry_time(YYYY-MM-DD HH:MM:SS)>")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
@router.message(Command("update_key_expiry"))
async def cmd_update_key_expiry(message: types.Message, is_admin: bool):
if is_admin:
try:
parts = message.text.split(maxsplit=2)
if len(parts) != 3:
await message.reply(
"Пожалуйста, используйте формат: /update_key_expiry <email> <expiry_time(YYYY-MM-DD HH:MM:SS)>"
)
return
_, email, expiry_time_str = parts
expiry_time = int(
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp()
* 1000
)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
return
await update_key_expiry(client_id, expiry_time)
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
if not record:
await message.reply("Клиент не найден в базе данных.")
return
server_id = record["server_id"]
tg_id = await get_tg_id_by_client_id(client_id)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
logger.info(
f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}"
)
success = await extend_client_key_admin(
session, server_id, tg_id, client_id, email, expiry_time
)
logger.info(
f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}"
)
if success:
await message.reply(
f"Время истечения ключа для клиента {client_id} ({email}) обновлено и синхронизировано с панелью."
)
else:
await message.reply(
f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью."
)
finally:
await conn.close()
except ValueError:
await message.reply(
"Пожалуйста, используйте формат: /update_key_expiry <email> <expiry_time(YYYY-MM-DD HH:MM:SS)>"
)
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
+155 -86
View File
@@ -2,12 +2,17 @@ import subprocess
from datetime import datetime
import asyncpg
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup, Message)
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from backup import backup_database
from bot import bot
@@ -16,107 +21,171 @@ from handlers.commands import send_message_to_all_clients
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
@router.message(Command('admin'))
async def handle_admin_command(message: types.Message):
if message.from_user.id != ADMIN_ID:
await bot.send_message(message.chat.id, "У вас нет доступа к этой команде.")
return
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")],
[InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")],
[InlineKeyboardButton(text="Отправить сообщение всем клиентам", callback_data="send_to_alls")],
[InlineKeyboardButton(text="Создать бэкап", callback_data="backups")],
[InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")]
])
await bot.send_message(message.chat.id, "Панель администратора", reply_markup=keyboard)
@router.callback_query(lambda c: c.data == "user_stats")
async def user_stats_menu(callback_query: CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
try:
total_users = await conn.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
active_keys = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE expiry_time > $1", int(datetime.utcnow().timestamp() * 1000))
expired_keys = total_keys - active_keys
stats_message = (
f"🔹 <b>Общая статистика пользователей:</b>\n"
f"• Всего пользователей: <b>{total_users}</b>\n"
f"• Всего ключей: <b>{total_keys}</b>\n"
f"• Всего рефералов: <b>{total_referrals}</b>\n"
f"• Активные ключи: <b>{active_keys}</b>\n"
f"• Истекшие ключи: <b>{expired_keys}</b>"
@router.message(Command("admin"))
async def handle_admin_command(message: types.Message, is_admin: bool):
if is_admin:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="Редактор пользователей", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="Отправить сообщение всем клиентам", callback_data="send_to_alls"
)
)
builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups"))
builder.row(
InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")
)
await bot.send_message(
message.chat.id, "Панель администратора.", reply_markup=builder.as_markup()
)
else:
await bot.send_message(message.chat.id, "У вас нет доступа к этой команде.")
back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[back_button]
])
await callback_query.message.edit_text(stats_message, reply_markup=keyboard, parse_mode="HTML")
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(lambda c: c.data == "send_to_alls")
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await send_message_to_all_clients(callback_query.message, state, from_panel=True)
await callback_query.answer()
@router.callback_query(lambda c: c.data == "backups")
async def handle_backup(message: Message):
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.callback_query(lambda c: c.data == "restart_bot")
async def handle_restart(callback_query: CallbackQuery):
if callback_query.from_user.id == ADMIN_ID:
@router.callback_query(F.data == "user_stats")
async def user_stats_menu(callback_query: CallbackQuery, is_admin: bool):
if is_admin:
conn = await asyncpg.connect(DATABASE_URL)
try:
result = subprocess.run(['sudo', 'systemctl', 'restart', 'bot.service'], check=True, capture_output=True, text=True)
total_users = await conn.fetchval("SELECT COUNT(*) FROM connections")
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
active_keys = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"🔹 <b>Общая статистика пользователей:</b>\n"
f"• Всего пользователей: <b>{total_users}</b>\n"
f"• Всего ключей: <b>{total_keys}</b>\n"
f"• Всего рефералов: <b>{total_referrals}</b>\n"
f"• Активные ключи: <b>{active_keys}</b>\n"
f"• Истекшие ключи: <b>{expired_keys}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")
)
await callback_query.message.edit_text(
stats_message, reply_markup=builder.as_markup(), parse_mode="HTML"
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(F.data == "send_to_alls")
async def handle_send_to_all(
callback_query: CallbackQuery, state: FSMContext, is_admin: bool
):
if is_admin:
await send_message_to_all_clients(
callback_query.message, state, from_panel=True
)
await callback_query.answer()
@router.callback_query(F.data == "backups")
async def handle_backup(message: Message, is_admin: bool):
if is_admin:
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.callback_query(F.data == "restart_bot")
async def handle_restart(callback_query: CallbackQuery, is_admin: bool):
if is_admin:
try:
result = subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
check=True,
capture_output=True,
text=True,
)
await callback_query.message.answer("Бот успешно перезапущен.")
except subprocess.CalledProcessError as e:
await callback_query.message.answer(f"Бот будет перезапущен через 30 секунд {e.stderr}")
else:
await callback_query.answer("У вас нет доступа к этой команде.", show_alert=True)
await callback_query.message.answer(
f"Бот будет перезапущен через 30 секунд {e.stderr}"
)
@router.callback_query(lambda c: c.data == "user_editor")
async def user_editor_menu(callback_query: CallbackQuery):
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Поиск по имени ключа", callback_data="search_by_key_name")],
[InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")],
[InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")] # Back button
])
await callback_query.message.edit_text("Выберите метод поиска:", reply_markup=keyboard)
@router.callback_query(lambda c: c.data == "back_to_admin_menu")
async def back_to_admin_menu(callback_query: CallbackQuery):
@router.callback_query(F.data == "user_editor")
async def user_editor_menu(callback_query: CallbackQuery, is_admin: bool):
if is_admin:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Поиск по имени ключа", callback_data="search_by_key_name"
)
)
builder.row(
InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")
)
builder.row(
InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")
)
await callback_query.message.edit_text(
"Выберите метод поиска:", reply_markup=builder.as_markup()
)
@router.callback_query(F.data == "back_to_admin_menu")
async def back_to_admin_menu(callback_query: CallbackQuery, is_admin: bool):
try:
await callback_query.message.delete()
except Exception:
pass
tg_id = callback_query.from_user.id
if tg_id == ADMIN_ID:
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")],
[InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")],
[InlineKeyboardButton(text="Отправить сообщение всем клиентам", callback_data="send_to_alls")],
[InlineKeyboardButton(text="Создать бэкап", callback_data="backups")],
[InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")]
])
await bot.send_message(tg_id, "Панель администратора", reply_markup=keyboard)
else:
await bot.send_message(tg_id, "У вас нет доступа к этой команде.")
if is_admin:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="Редактор пользователей", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="Отправить сообщение всем клиентам",
callback_data="send_to_alls",
)
)
builder.row(InlineKeyboardButton(text="Создать бэкап", callback_data="backups"))
builder.row(
InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")
)
await bot.send_message(
tg_id, "Панель администратора", reply_markup=builder.as_markup()
)
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.edit_message_text(
message, chat_id=tg_id, message_id=callback_query.message.message_id
)
+242 -108
View File
@@ -1,50 +1,51 @@
import asyncio
from datetime import datetime
import asyncio
import logging
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup)
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup
from loguru import logger
from auth import login_with_credentials
from bot import bot
from client import delete_client, extend_client_key_admin
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS
from database import (get_client_id_by_email, get_tg_id_by_client_id,
update_key_expiry)
from database import get_client_id_by_email, get_tg_id_by_client_id, update_key_expiry
from handlers.admin.admin_panel import back_to_admin_menu
from handlers.utils import sanitize_key_name
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
waiting_for_new_balance = State()
waiting_for_new_balance = State()
waiting_for_key_name = State()
waiting_for_expiry_time = State()
waiting_for_expiry_time = State()
@router.callback_query(lambda c: c.data == "search_by_tg_id")
@router.callback_query(F.data == "search_by_tg_id")
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("Введите tg_id клиента:")
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit())
async def handle_tg_id_input(message: types.Message, state: FSMContext):
tg_id = int(message.text)
conn = await asyncpg.connect(DATABASE_URL)
try:
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
balance = await conn.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
key_records = await conn.fetch("SELECT email FROM keys WHERE tg_id = $1", tg_id)
referral_count = await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
referral_count = await conn.fetchval(
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
)
if balance is None:
await message.reply("Пользователь с указанным tg_id не найден.")
@@ -55,11 +56,22 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext):
[InlineKeyboardButton(text=email, callback_data=f"edit_key_{email}")]
for email, in key_records
]
keyboard = InlineKeyboardMarkup(inline_keyboard=[
*key_buttons,
[InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")],
[InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")]
])
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
*key_buttons,
[
InlineKeyboardButton(
text="📝 Изменить баланс",
callback_data=f"change_balance_{tg_id}",
)
],
[
InlineKeyboardButton(
text="Назад", callback_data="back_to_user_editor"
)
],
]
)
user_info = (
f"Информация о пользователе:\n"
@@ -73,32 +85,40 @@ async def handle_tg_id_input(message: types.Message, state: FSMContext):
finally:
await conn.close()
@router.callback_query(lambda c: c.data.startswith('change_balance_'))
@router.callback_query(F.data.startswith("change_balance_"))
async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
tg_id = int(callback_query.data.split('_')[2])
await state.update_data(tg_id=tg_id)
tg_id = int(callback_query.data.split("_")[2])
await state.update_data(tg_id=tg_id)
await callback_query.message.edit_text("Введите новую сумму баланса:")
await callback_query.answer()
await state.set_state(UserEditorState.waiting_for_new_balance)
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance)
async def handle_new_balance_input(message: types.Message, state: FSMContext):
if not message.text.isdigit() or int(message.text) < 0:
await message.reply("Пожалуйста, введите корректную сумму для изменения баланса.")
await message.reply(
"Пожалуйста, введите корректную сумму для изменения баланса."
)
return
new_balance = int(message.text)
user_data = await state.get_data()
tg_id = user_data.get('tg_id')
tg_id = user_data.get("tg_id")
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute("UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id)
await conn.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id
)
response_message = f"Баланс успешно изменен на <b>{new_balance}</b>."
back_button = InlineKeyboardButton(text="Назад в меню админа", callback_data="back_to_user_editor")
back_button = InlineKeyboardButton(
text="Назад в меню админа", callback_data="back_to_user_editor"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML")
@@ -106,27 +126,32 @@ async def handle_new_balance_input(message: types.Message, state: FSMContext):
finally:
await conn.close()
await state.clear()
await state.clear()
@router.callback_query(lambda c: c.data.startswith('edit_key_'))
@router.callback_query(F.data.startswith("edit_key_"))
async def process_key_edit(callback_query: CallbackQuery):
email = callback_query.data.split('_', 2)[2]
email = callback_query.data.split("_", 2)[2]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('''
record = await conn.fetchrow(
"""
SELECT k.key, k.expiry_time, k.server_id
FROM keys k
WHERE k.email = $1
''', email)
""",
email,
)
if record:
key = record['key']
expiry_time = record['expiry_time']
server_id = record['server_id']
server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер')
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = SERVERS.get(server_id, {}).get(
"name", "Неизвестный сервер"
)
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
@@ -140,7 +165,7 @@ async def process_key_edit(callback_query: CallbackQuery):
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime('%d %B %Y года')
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = (
f"Ключ: <pre>{key}</pre>\n"
@@ -149,45 +174,67 @@ async def process_key_edit(callback_query: CallbackQuery):
f"Сервер: <b>{server_name}</b>"
)
change_expiry_button = types.InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}')
delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}')
change_expiry_button = types.InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
delete_button = types.InlineKeyboardButton(
text="❌ Удалить ключ", callback_data=f"delete_key_admin|{email}"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[change_expiry_button, delete_button],
[InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] # Кнопка "Назад"
[
InlineKeyboardButton(
text="Назад", callback_data="back_to_user_editor"
)
], # Кнопка "Назад"
]
)
await callback_query.message.edit_text(response_message, reply_markup=keyboard, parse_mode="HTML")
await callback_query.message.edit_text(
response_message, reply_markup=keyboard, parse_mode="HTML"
)
else:
await callback_query.message.edit_text("<b>Информация о ключе не найдена.</b>", parse_mode="HTML")
await callback_query.message.edit_text(
"<b>Информация о ключе не найдена.</b>", parse_mode="HTML"
)
finally:
await conn.close()
except Exception as e:
await handle_error(callback_query.from_user.id, callback_query, f"Ошибка при получении информации о ключе: {e}")
await handle_error(
callback_query.from_user.id,
callback_query,
f"Ошибка при получении информации о ключе: {e}",
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == "search_by_key_name")
@router.callback_query(F.data == "search_by_key_name")
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("Введите имя ключа:")
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name)
async def handle_key_name_input(message: types.Message, state: FSMContext):
key_name = sanitize_key_name(message.text)
conn = await asyncpg.connect(DATABASE_URL)
try:
user_records = await conn.fetch('''
user_records = await conn.fetch(
"""
SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.email = $1
''', key_name)
""",
key_name,
)
if not user_records:
await message.reply("Пользователь с указанным именем ключа не найден.")
@@ -198,15 +245,17 @@ async def handle_key_name_input(message: types.Message, state: FSMContext):
key_buttons = []
for record in user_records:
tg_id = record['tg_id']
balance = record['balance']
email = record['email']
key = record['key']
expiry_time = record['expiry_time']
server_id = record['server_id']
server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер')
tg_id = record["tg_id"]
balance = record["balance"]
email = record["email"]
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = SERVERS.get(server_id, {}).get("name", "Неизвестный сервер")
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%d %B %Y')
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime(
"%d %B %Y"
)
response_messages.append(
f"Ключ: <pre>{key}</pre>\n"
@@ -215,36 +264,47 @@ async def handle_key_name_input(message: types.Message, state: FSMContext):
f"Сервер: <b>{server_name}</b>"
)
change_expiry_button = InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}')
delete_button = InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}')
change_expiry_button = InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
delete_button = InlineKeyboardButton(
text="❌ Удалить ключ", callback_data=f"delete_key_admin|{email}"
)
key_buttons.append([change_expiry_button, delete_button])
key_buttons.append([InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")])
key_buttons.append(
[InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")]
)
keyboard = InlineKeyboardMarkup(inline_keyboard=key_buttons)
await message.reply("\n".join(response_messages), reply_markup=keyboard, parse_mode="HTML")
await message.reply(
"\n".join(response_messages), reply_markup=keyboard, parse_mode="HTML"
)
finally:
await conn.close()
await state.clear()
@router.callback_query(lambda c: c.data.startswith('change_expiry|'))
@router.callback_query(F.data.startswith("change_expiry|"))
async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
email = callback_query.data.split('|')[1]
email = callback_query.data.split("|")[1]
await callback_query.message.edit_text(
f"Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:",
parse_mode="HTML"
)
f"Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:",
parse_mode="HTML",
)
await state.update_data(email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(UserEditorState.waiting_for_expiry_time)
async def handle_expiry_time_input(message: types.Message, state: FSMContext):
user_data = await state.get_data()
email = user_data.get('email')
email = user_data.get("email")
if not email:
await message.reply("Email не найден в состоянии.")
@@ -253,9 +313,11 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext):
try:
expiry_time_str = message.text
expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000)
expiry_time = int(
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000
)
client_id = await get_client_id_by_email(email)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
await state.clear()
@@ -263,7 +325,9 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext):
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id)
record = await conn.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
if not record:
await message.reply("Клиент не найден в базе данных.")
await state.clear()
@@ -274,23 +338,29 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext):
async def update_key_on_all_servers():
tasks = []
for server_id in SERVERS:
tasks.append(asyncio.create_task(
renew_server_key(server_id, tg_id, client_id, email, expiry_time)
))
tasks.append(
asyncio.create_task(
renew_server_key(
server_id, tg_id, client_id, email, expiry_time
)
)
)
await asyncio.gather(*tasks)
await update_key_on_all_servers()
await update_key_expiry(client_id, expiry_time)
response_message = (
f"Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
)
response_message = f"Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")
back_button = InlineKeyboardButton(
text="Назад", callback_data="back_to_user_editor"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML")
await message.reply(
response_message, reply_markup=keyboard, parse_mode="HTML"
)
finally:
await conn.close()
@@ -302,51 +372,87 @@ async def handle_expiry_time_input(message: types.Message, state: FSMContext):
await state.clear()
async def renew_server_key(server_id, tg_id, client_id, email, new_expiry_time):
try:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
await extend_client_key_admin(session, server_id, tg_id, client_id, email, new_expiry_time)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
await extend_client_key_admin(
session, server_id, tg_id, client_id, email, new_expiry_time
)
except Exception as e:
logger.error(f"Не удалось обновить ключ {client_id} на сервере {server_id}: {e}")
logger.error(
f"Не удалось обновить ключ {client_id} на сервере {server_id}: {e}"
)
@router.callback_query(lambda c: c.data.startswith('delete_key_admin|'))
@router.callback_query(F.data.startswith("delete_key_admin|"))
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
email = callback_query.data.split('|')[1]
email = callback_query.data.split("|")[1]
conn = await asyncpg.connect(DATABASE_URL)
try:
client_id = await conn.fetchval('SELECT client_id FROM keys WHERE email = $1', email)
client_id = await conn.fetchval(
"SELECT client_id FROM keys WHERE email = $1", email
)
if client_id is None:
await bot.edit_message_text("Ключ не найден.", chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.edit_message_text(
"Ключ не найден.",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
return
confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete_admin|{client_id}')],
[types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')]
])
confirmation_keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete_admin|{client_id}",
)
],
[
types.InlineKeyboardButton(
text="❌ Нет, отменить", callback_data="view_keys"
)
],
]
)
await bot.edit_message_text("<b>Вы уверены, что хотите удалить ключ?</b>", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard, parse_mode="HTML")
await bot.edit_message_text(
"<b>Вы уверены, что хотите удалить ключ?</b>",
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=confirmation_keyboard,
parse_mode="HTML",
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('confirm_delete_admin|'))
@router.callback_query(F.data.startswith("confirm_delete_admin|"))
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
client_id = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email FROM keys WHERE client_id = $1', client_id)
record = await conn.fetchrow(
"SELECT email FROM keys WHERE client_id = $1", client_id
)
if record:
email = record['email']
email = record["email"]
response_message = "Ключ успешно удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
async def delete_key_from_servers():
@@ -355,50 +461,78 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tasks.append(delete_key_from_server(server_id, client_id))
await asyncio.gather(*tasks)
await delete_key_from_servers()
await delete_key_from_db(client_id)
await delete_key_from_servers()
await delete_key_from_db(client_id)
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(f"Ошибка при удалении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.edit_message_text(
f"Ошибка при удалении ключа: {e}",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
await callback_query.answer()
async def delete_key_from_server(server_id, client_id):
"""Удаление ключа с сервера"""
try:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
success = await delete_client(session, server_id, client_id)
if not success:
logger.error(f"Ошибка удаления ключа {client_id} на сервере {server_id}")
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}")
logger.error(
f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}"
)
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
@router.callback_query(lambda c: c.data == "back_to_user_editor")
@router.callback_query(F.data == "back_to_user_editor")
async def back_to_user_editor(callback_query: CallbackQuery):
await back_to_admin_menu(callback_query)
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML")
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(
message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
parse_mode="HTML",
)
+103 -78
View File
@@ -4,132 +4,157 @@ from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
from loguru import logger
from bot import bot
from config import ADMIN_ID, DATABASE_URL
from handlers.admin.admin import cmd_add_balance
from handlers.keys.key_management import handle_key_name_input
from handlers.payment.pay import (ReplenishBalanceState,
process_custom_amount_input)
from handlers.payment.yookassa_pay import (
ReplenishBalanceState,
process_custom_amount_input,
)
from handlers.profile import process_callback_view_profile
from handlers.start import start_command
from handlers.texts import TRIAL
router = Router()
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
@router.message(Command('backup'))
async def backup_command(message: Message):
if message.from_user.id != ADMIN_ID:
await message.answer("У вас нет прав для выполнения этой команды.")
return
from backup import backup_database
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.message(Command("backup"))
async def backup_command(message: Message, is_admin: bool):
if is_admin:
from backup import backup_database
@router.message(Command('start'))
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.message(Command("start"))
async def handle_start(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command('add_balance'))
@router.message(Command("add_balance"))
async def handle_add_balance(message: types.Message, state: FSMContext):
await cmd_add_balance(message)
@router.message(Command('menu'))
@router.message(Command("menu"))
async def handle_menu(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command('send_trial'))
async def handle_send_trial_command(message: types.Message, state: FSMContext):
# Проверка на администратора
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
try:
conn = await asyncpg.connect(DATABASE_URL)
@router.message(Command("send_trial"))
async def handle_send_trial_command(
message: types.Message, state: FSMContext, is_admin: bool
):
if is_admin:
try:
records = await conn.fetch('''
SELECT tg_id FROM connections WHERE trial = 0
''')
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch(
"""
SELECT tg_id FROM connections WHERE trial = 0
"""
)
if records:
for record in records:
tg_id = record['tg_id']
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
print(f"Бот заблокирован пользователем с tg_id: {tg_id}")
else:
print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}")
if records:
for record in records:
tg_id = record["tg_id"]
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
logger.info(
f"Бот заблокирован пользователем с tg_id: {tg_id}"
)
else:
logger.error(
f"Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer("Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом.")
else:
await message.answer("Нет пользователей с не использованными пробными ключами.")
await message.answer(
"Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом."
)
else:
await message.answer(
"Нет пользователей с не использованными пробными ключами."
)
finally:
await conn.close()
except Exception as e:
await message.answer(f"Ошибка при отправке сообщений: {e}")
@router.message(Command("send_to_all"))
async def send_message_to_all_clients(
message: types.Message, state: FSMContext, is_admin: bool, from_panel=False
):
if from_panel and is_admin:
await message.answer(
"Введите текст сообщения, который вы хотите отправить всем клиентам:"
)
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message)
async def process_message_to_all(
message: types.Message, state: FSMContext, is_admin: bool
):
if is_admin:
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch("SELECT tg_id FROM connections")
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
except Exception as e:
logger.error(
f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя."
)
await message.answer("Сообщение было отправлено всем клиентам.")
except Exception as e:
logger.error(f"Ошибка при подключении к базе данных: {e}")
await message.answer("Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
except Exception as e:
await message.answer(f"Ошибка при отправке сообщений: {e}")
@router.message(Command('send_to_all'))
async def send_message_to_all_clients(message: types.Message, state: FSMContext, from_panel=False):
if not from_panel and message.from_user.id != ADMIN_ID:
await message.answer("У вас нет прав для выполнения этой команды.")
return
await message.answer("Введите текст сообщения, который вы хотите отправить всем клиентам:")
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message)
async def process_message_to_all(message: types.Message, state: FSMContext):
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch('SELECT tg_id FROM connections')
for record in tg_ids:
tg_id = record['tg_id']
try:
await bot.send_message(chat_id=tg_id, text=text_message)
except Exception as e:
print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя.")
await message.answer("Сообщение было отправлено всем клиентам.")
except Exception as e:
print(f"Ошибка при подключении к базе данных: {e}")
await message.answer("Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await state.clear()
@router.message()
async def handle_text(message: types.Message, state: FSMContext):
current_state = await state.get_state()
if message.text in ["/send_to_all"]:
await send_message_to_all_clients(message, state)
await send_message_to_all_clients(message, state)
return
if message.text == "Мой профиль":
callback_query = types.CallbackQuery(
id="1",
from_user=message.from_user,
chat_instance='',
data='view_profile',
message=message
chat_instance="",
data="view_profile",
message=message,
)
await process_callback_view_profile(callback_query, state)
return
@@ -146,5 +171,5 @@ async def handle_text(message: types.Message, state: FSMContext):
await backup_command(message)
return
elif current_state is None:
elif current_state is None:
await start_command(message)
+8 -11
View File
@@ -1,8 +1,7 @@
import os
from aiogram import types
from aiogram.types import (BufferedInputFile, InlineKeyboardButton,
InlineKeyboardMarkup)
from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from handlers.texts import INSTRUCTIONS
@@ -10,26 +9,24 @@ from handlers.texts import INSTRUCTIONS
async def send_instructions(callback_query: types.CallbackQuery):
await callback_query.message.delete()
instructions_message = (
INSTRUCTIONS
)
instructions_message = INSTRUCTIONS
image_path = os.path.join(os.path.dirname(__file__), 'instructions.jpg')
image_path = os.path.join(os.path.dirname(__file__), "instructions.jpg")
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
await callback_query.answer()
return
back_button = InlineKeyboardButton(text='🔙 Назад', callback_data='back_to_main')
back_button = InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_main")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
with open(image_path, 'rb') as image_from_buffer:
with open(image_path, "rb") as image_from_buffer:
await callback_query.message.answer_photo(
BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"),
caption=instructions_message,
parse_mode='Markdown',
reply_markup=keyboard
parse_mode="Markdown",
reply_markup=keyboard,
)
await callback_query.answer()
+142 -66
View File
@@ -1,5 +1,4 @@
import asyncio
import logging
import uuid
from datetime import datetime, timedelta
@@ -7,24 +6,39 @@ import asyncpg
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup, Message)
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from loguru import logger
from auth import login_with_credentials
from bot import bot, dp
from client import add_client
from config import (ADMIN_PASSWORD, ADMIN_USERNAME, APP_URL, DATABASE_URL,
PUBLIC_LINK, SERVERS)
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
APP_URL,
DATABASE_URL,
PUBLIC_LINK,
SERVERS,
)
from database import add_connection, get_balance, store_key, update_balance
from handlers.instructions.instructions import send_instructions
from handlers.profile import process_callback_view_profile
from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, key_message_success
from handlers.texts import (
KEY,
KEY_TRIAL,
NULL_BALANCE,
RENEWAL_PLANS,
key_message_success,
)
from handlers.utils import sanitize_key_name
router = Router()
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class Form(StatesGroup):
waiting_for_server_selection = State()
@@ -32,14 +46,17 @@ class Form(StatesGroup):
viewing_profile = State()
waiting_for_message = State()
@dp.callback_query(F.data == 'create_key')
@dp.callback_query(F.data == "create_key")
async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
pass
server_id = "все сервера"
await state.update_data(selected_server_id=server_id)
@@ -47,81 +64,102 @@ async def process_callback_create_key(callback_query: CallbackQuery, state: FSMC
await callback_query.answer()
async def select_server(callback_query: CallbackQuery, state: FSMContext):
selected_server_id = (await state.get_data()).get("selected_server_id")
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', callback_query.from_user.id)
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1",
callback_query.from_user.id,
)
finally:
await conn.close()
trial_status = existing_connection['trial'] if existing_connection else 0
trial_status = existing_connection["trial"] if existing_connection else 0
if trial_status == 1:
await bot.send_message(
chat_id=callback_query.from_user.id,
text=KEY,
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='✅ Да, подключить новое устройство', callback_data='confirm_create_new_key')],
[InlineKeyboardButton(text='↩️ Назад', callback_data='cancel_create_key')]
])
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Да, подключить новое устройство",
callback_data="confirm_create_new_key",
)
],
[
InlineKeyboardButton(
text="↩️ Назад", callback_data="cancel_create_key"
)
],
]
),
)
await state.update_data(creating_new_key=True)
else:
await bot.send_message(
chat_id=callback_query.from_user.id,
text=KEY_TRIAL,
parse_mode="HTML"
chat_id=callback_query.from_user.id, text=KEY_TRIAL, parse_mode="HTML"
)
await state.set_state(Form.waiting_for_key_name)
await callback_query.answer()
@dp.callback_query(F.data == 'confirm_create_new_key')
@dp.callback_query(F.data == "confirm_create_new_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
data = await state.get_data()
server_id = data.get('selected_server_id')
server_id = data.get("selected_server_id")
balance = await get_balance(tg_id)
if balance < 100:
replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await callback_query.message.edit_text(
NULL_BALANCE,
reply_markup=keyboard
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard)
await state.clear()
return
await callback_query.message.edit_text("🔑 Пожалуйста, введите имя подключаемого устройства:")
await callback_query.message.edit_text(
"🔑 Пожалуйста, введите имя подключаемого устройства:"
)
await state.set_state(Form.waiting_for_key_name)
await state.update_data(creating_new_key=True)
await callback_query.answer()
@dp.callback_query(F.data == 'cancel_create_key')
@dp.callback_query(F.data == "cancel_create_key")
async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
await callback_query.answer()
async def handle_key_name_input(message: Message, state: FSMContext):
tg_id = message.from_user.id
key_name = sanitize_key_name(message.text)
if not key_name:
await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите устройство на английском языке.")
await message.bot.send_message(
tg_id, "📝 Пожалуйста, назовите устройство на английском языке."
)
return
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_key = await conn.fetchrow('SELECT * FROM keys WHERE email = $1', key_name.lower())
existing_key = await conn.fetchrow(
"SELECT * FROM keys WHERE email = $1", key_name.lower()
)
if existing_key:
await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.")
await message.bot.send_message(
tg_id,
"❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.",
)
await state.set_state(Form.waiting_for_key_name)
return
finally:
@@ -135,90 +173,127 @@ async def handle_key_name_input(message: Message, state: FSMContext):
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', tg_id)
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
finally:
await conn.close()
trial_status = existing_connection['trial'] if existing_connection else 0
trial_status = existing_connection["trial"] if existing_connection else 0
if trial_status == 0:
expiry_time = current_time + timedelta(days=1, hours=3)
else:
balance = await get_balance(tg_id)
if balance < 100:
replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await message.bot.send_message(tg_id, "❗️ Недостаточно средств на балансе для создания подписки на новое устройство.", reply_markup=keyboard)
await message.bot.send_message(
tg_id,
"❗️ Недостаточно средств на балансе для создания подписки на новое устройство.",
reply_markup=keyboard,
)
await state.clear()
return
await update_balance(tg_id, -100)
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
expiry_time = current_time + timedelta(days=30, hours=3)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}"
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
button_profile = InlineKeyboardButton(
text="👤 Мой профиль", callback_data="view_profile"
)
button_iphone = InlineKeyboardButton(
text='🍏 Подключить',
url=f'{APP_URL}/?url=v2raytun://import/{public_link}'
text="🍏 Подключить", url=f"{APP_URL}/?url=v2raytun://import/{public_link}"
)
button_android = InlineKeyboardButton(
text='🤖 Подключить',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={public_link}'
text="🤖 Подключить",
url=f"{APP_URL}/?url=v2raytun://import-sub?url={public_link}",
)
button_download_ios = InlineKeyboardButton(
text='🍏 Скачать',
url="https://apps.apple.com/ru/app/v2raytun/id6476628951"
text="🍏 Скачать", url="https://apps.apple.com/ru/app/v2raytun/id6476628951"
)
button_download_android = InlineKeyboardButton(
text='🤖 Скачать',
url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru"
text="🤖 Скачать",
url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru",
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[button_download_ios, button_download_android],
[button_iphone, button_android],
[button_profile]
])
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[button_download_ios, button_download_android],
[button_iphone, button_android],
[button_profile],
]
)
remaining_time = expiry_time - current_time
days = remaining_time.days
key_message = key_message_success(public_link, f"Оставшееся время ключа: {days} день")
key_message = key_message_success(
public_link, f"Оставшееся время ключа: {days} день"
)
await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard)
await message.bot.send_message(
tg_id, key_message, parse_mode="HTML", reply_markup=keyboard
)
try:
tasks = []
for server_id in SERVERS:
tasks.append(asyncio.create_task(create_key_on_server(server_id, tg_id, client_id, email, expiry_timestamp)))
tasks.append(
asyncio.create_task(
create_key_on_server(
server_id, tg_id, client_id, email, expiry_timestamp
)
)
)
await asyncio.gather(*tasks)
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id)
existing_connection = await conn.fetchrow(
"SELECT * FROM connections WHERE tg_id = $1", tg_id
)
if existing_connection:
await conn.execute('UPDATE connections SET trial = 1 WHERE tg_id = $1', tg_id)
await conn.execute(
"UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
)
else:
await add_connection(tg_id, 0, 1)
finally:
await conn.close()
await store_key(tg_id, client_id, email, expiry_timestamp, public_link, 'all_servers')
await store_key(
tg_id, client_id, email, expiry_timestamp, public_link, "all_servers"
)
except Exception as e:
await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}")
await state.clear()
async def create_key_on_server(server_id, tg_id, client_id, email, expiry_timestamp):
try:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
response = await add_client(
session, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0,
expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision"
session,
server_id,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
)
if not response.get("success", True):
error_msg = response.get("msg", "Неизвестная ошибка.")
@@ -230,11 +305,12 @@ async def create_key_on_server(server_id, tg_id, client_id, email, expiry_timest
logger.error(f"Ошибка на сервере {server_id}: {e}")
@dp.callback_query(F.data == 'instructions')
@dp.callback_query(F.data == "instructions")
async def handle_instructions(callback_query: CallbackQuery):
await send_instructions(callback_query)
await send_instructions(callback_query)
@dp.callback_query(F.data == 'back_to_main')
@dp.callback_query(F.data == "back_to_main")
async def handle_back_to_main(callback_query: CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
await callback_query.answer()
+385 -157
View File
@@ -1,51 +1,74 @@
import asyncio
import locale
import logging
import os
from datetime import datetime, timedelta
import asyncpg
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile
from loguru import logger
from auth import login_with_credentials
from bot import bot
from client import add_client, delete_client, extend_client_key
from config import (ADMIN_PASSWORD, ADMIN_USERNAME, APP_URL, DATABASE_URL,
PUBLIC_LINK, SERVERS)
from database import (delete_key, get_balance, store_key, update_balance,
update_key_expiry)
from handlers.texts import (INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, NO_KEYS,
PLAN_SELECTION_MSG, RENEWAL_PLANS,
SUCCESS_RENEWAL_MSG, key_message)
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
APP_URL,
DATABASE_URL,
PUBLIC_LINK,
SERVERS,
)
from database import (
delete_key,
get_balance,
store_key,
update_balance,
update_key_expiry,
)
from handlers.texts import (
INSUFFICIENT_FUNDS_MSG,
KEY_NOT_FOUND_MSG,
NO_KEYS,
PLAN_SELECTION_MSG,
RENEWAL_PLANS,
SUCCESS_RENEWAL_MSG,
key_message,
)
locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8')
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
router = Router()
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
@router.callback_query(lambda c: c.data == 'view_keys')
@router.callback_query(F.data == "view_keys")
async def process_callback_view_keys(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch('''
records = await conn.fetch(
"""
SELECT email, client_id FROM keys WHERE tg_id = $1
''', tg_id)
""",
tg_id,
)
if records:
buttons = []
for record in records:
key_name = record['email']
client_id = record['client_id']
button = types.InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f'view_key|{key_name}|{client_id}')
key_name = record["email"]
client_id = record["client_id"]
button = types.InlineKeyboardButton(
text=f"🔑 {key_name}",
callback_data=f"view_key|{key_name}|{client_id}",
)
buttons.append([button])
back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')
back_button = types.InlineKeyboardButton(
text="🔙 Назад", callback_data="view_profile"
)
buttons.append([back_button])
inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons)
@@ -54,28 +77,38 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
"<i>Нажмите на имя устройства для управления его подпиской.</i>"
)
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=inline_keyboard,
parse_mode="HTML"
parse_mode="HTML",
)
else:
response_message = NO_KEYS
create_key_button = types.InlineKeyboardButton(text=' Создать ключ', callback_data='create_key')
back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]])
response_message = NO_KEYS
create_key_button = types.InlineKeyboardButton(
text=" Создать ключ", callback_data="create_key"
)
back_button = types.InlineKeyboardButton(
text="🔙 Назад", callback_data="view_profile"
)
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[create_key_button], [back_button]]
)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=keyboard,
parse_mode="HTML"
parse_mode="HTML",
)
finally:
@@ -86,31 +119,41 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('view_key|'))
@router.callback_query(F.data.startswith("view_key|"))
async def process_callback_view_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
key_name, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2]
key_name, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
)
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('''
record = await conn.fetchrow(
"""
SELECT k.expiry_time, k.server_id, k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
''', tg_id, key_name)
""",
tg_id,
key_name,
)
if record:
key = record['key']
expiry_time = record['expiry_time']
server_id = record['server_id']
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = SERVERS.get(server_id, {}).get('name', 'мультисервер')
server_name = SERVERS.get(server_id, {}).get("name", "мультисервер")
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
@@ -123,259 +166,387 @@ async def process_callback_view_key(callback_query: types.CallbackQuery):
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime('%d %B %Y года')
response_message = key_message(key, formatted_expiry_date, days_left_message, server_name)
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(
key, formatted_expiry_date, days_left_message, server_name
)
download_android_button = types.InlineKeyboardButton(
text='🤖 Скачать',
url='https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru'
text="🤖 Скачать",
url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru",
)
download_iphone_button = types.InlineKeyboardButton(
text='🍏 Скачать',
url='https://apps.apple.com/ru/app/v2raytun/id6476628951'
text="🍏 Скачать",
url="https://apps.apple.com/ru/app/v2raytun/id6476628951",
)
connect_iphone_button = types.InlineKeyboardButton(
text='🍏 Подключить',
url=f'{APP_URL}/?url=v2raytun://import/{key}'
text="🍏 Подключить", url=f"{APP_URL}/?url=v2raytun://import/{key}"
)
connect_android_button = types.InlineKeyboardButton(
text='🤖 Подключить',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={key}'
text="🤖 Подключить",
url=f"{APP_URL}/?url=v2raytun://import-sub?url={key}",
)
renew_button = types.InlineKeyboardButton(text='⏳ Продлить', callback_data=f'renew_key|{client_id}')
delete_button = types.InlineKeyboardButton(text='❌ Удалить', callback_data=f'delete_key|{client_id}')
back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile')
renew_button = types.InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{client_id}"
)
delete_button = types.InlineKeyboardButton(
text="❌ Удалить", callback_data=f"delete_key|{client_id}"
)
back_button = types.InlineKeyboardButton(
text="🔙 Назад в профиль", callback_data="view_profile"
)
inline_keyboard = [
[download_iphone_button, download_android_button],
[connect_iphone_button, connect_android_button],
[renew_button, delete_button],
[connect_iphone_button, connect_android_button],
[renew_button, delete_button],
]
if not key.startswith(PUBLIC_LINK):
update_subscription_button = types.InlineKeyboardButton(text='🔄 Обновить подписку', callback_data=f'update_subscription|{client_id}')
inline_keyboard.append([update_subscription_button])
update_subscription_button = types.InlineKeyboardButton(
text="🔄 Обновить подписку",
callback_data=f"update_subscription|{client_id}",
)
inline_keyboard.append([update_subscription_button])
inline_keyboard.append([back_button])
inline_keyboard.append([back_button])
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
image_path = os.path.join(os.path.dirname(__file__), 'pic_view.jpg')
image_path = os.path.join(os.path.dirname(__file__), "pic_view.jpg")
if not os.path.isfile(image_path):
await bot.send_message(tg_id, "Файл изображения не найден.")
return
with open(image_path, 'rb') as image_file:
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(image_file.read(), filename="pic_view.jpg"),
photo=BufferedInputFile(
image_file.read(), filename="pic_view.jpg"
),
caption=response_message,
reply_markup=keyboard,
parse_mode="HTML"
parse_mode="HTML",
)
else:
await bot.send_message(chat_id=tg_id, text="<b>Информация о подписке не найдена.</b>", parse_mode="HTML")
await bot.send_message(
chat_id=tg_id,
text="<b>Информация о подписке не найдена.</b>",
parse_mode="HTML",
)
finally:
await conn.close()
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}")
await handle_error(
tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}"
)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('update_subscription|'))
@router.callback_query(F.data.startswith("update_subscription|"))
async def process_callback_update_subscription(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
client_id = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('''
record = await conn.fetchrow(
"""
SELECT k.key, k.expiry_time, k.email, k.server_id
FROM keys k
WHERE k.tg_id = $1 AND k.client_id = $2
''', tg_id, client_id)
""",
tg_id,
client_id,
)
if record:
expiry_time = record['expiry_time']
email = record['email']
expiry_time = record["expiry_time"]
email = record["email"]
public_link = f"{PUBLIC_LINK}{email}"
try:
await conn.execute('''
await conn.execute(
"""
DELETE FROM keys
WHERE tg_id = $1 AND client_id = $2
''', tg_id, client_id)
""",
tg_id,
client_id,
)
except Exception as delete_error:
await bot.send_message(tg_id, f"Ошибка при удалении старой подписки: {delete_error}")
await bot.send_message(
tg_id, f"Ошибка при удалении старой подписки: {delete_error}"
)
return
tasks = []
for server_id in SERVERS:
tasks.append(update_key_on_server(tg_id, client_id, email, expiry_time, server_id))
tasks.append(
update_key_on_server(
tg_id, client_id, email, expiry_time, server_id
)
)
results = await asyncio.gather(*tasks)
await store_key(tg_id, client_id, email, expiry_time, public_link, server_id='все сервера')
await store_key(
tg_id,
client_id,
email,
expiry_time,
public_link,
server_id="все сервера",
)
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
response_message = f"Ваша подписка {email} обновлена!"
back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile')
back_button = types.InlineKeyboardButton(
text="🔙 Назад в профиль", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.send_message(
tg_id,
response_message,
reply_markup=keyboard,
parse_mode="HTML"
tg_id, response_message, reply_markup=keyboard, parse_mode="HTML"
)
else:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await bot.send_message(
tg_id,
"<b>Ключ не найден в базе данных.</b>",
parse_mode="HTML"
tg_id, "<b>Ключ не найден в базе данных.</b>", parse_mode="HTML"
)
finally:
await conn.close()
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при обновлении подписки: {e}")
await handle_error(
tg_id, callback_query, f"Ошибка при обновлении подписки: {e}"
)
await callback_query.answer()
async def update_key_on_server(tg_id, client_id, email, expiry_time, server_id):
try:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
response = await add_client(
session, server_id, client_id, email, tg_id,
limit_ip=1, total_gb=0, expiry_time=expiry_time,
enable=True, flow="xtls-rprx-vision"
session,
server_id,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_time,
enable=True,
flow="xtls-rprx-vision",
)
if not response.get("success"):
logger.error(f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}")
logger.error(
f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}"
)
else:
logger.info(f"Ключ успешно обновлен на сервере {server_id} для {client_id}")
except Exception as e:
logger.error(f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}: {e}")
logger.error(
f"Ошибка при обновлении ключа на сервере {server_id} для {client_id}: {e}"
)
@router.callback_query(lambda c: c.data.startswith('delete_key|'))
@router.callback_query(F.data.startswith("delete_key|"))
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
client_id = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
pass
confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete|{client_id}')],
[types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')]
])
confirmation_keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete|{client_id}",
)
],
[
types.InlineKeyboardButton(
text="❌ Нет, отменить", callback_data="view_keys"
)
],
]
)
await bot.send_message(
chat_id=tg_id,
text="<b>Вы уверены, что хотите удалить ключ?</b>",
reply_markup=confirmation_keyboard,
parse_mode="HTML"
parse_mode="HTML",
)
except Exception as e:
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при удалении ключа:</b> {e}",
parse_mode="HTML"
parse_mode="HTML",
)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('renew_key|'))
@router.callback_query(F.data.startswith("renew_key|"))
async def process_callback_renew_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
client_id = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
pass
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id)
record = await conn.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1", client_id
)
if record:
email = record['email']
expiry_time = record['expiry_time']
current_time = datetime.utcnow().timestamp() * 1000
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', callback_data=f'renew_plan|1|{client_id}')],
[types.InlineKeyboardButton(text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', callback_data=f'renew_plan|3|{client_id}')],
[types.InlineKeyboardButton(text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', callback_data=f'renew_plan|6|{client_id}')],
[types.InlineKeyboardButton(text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', callback_data=f'renew_plan|12|{client_id}')],
[types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')]
])
email = record["email"]
expiry_time = record["expiry_time"]
current_time = datetime.utcnow().timestamp() * 1000
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
callback_data=f"renew_plan|1|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)',
callback_data=f"renew_plan|3|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)',
callback_data=f"renew_plan|6|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)',
callback_data=f"renew_plan|12|{client_id}",
)
],
[
types.InlineKeyboardButton(
text="🔙 Назад", callback_data="view_profile"
)
],
]
)
balance = await get_balance(tg_id)
response_message = PLAN_SELECTION_MSG.format(balance=balance, expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S'))
response_message = PLAN_SELECTION_MSG.format(
balance=balance,
expiry_date=datetime.utcfromtimestamp(expiry_time / 1000).strftime(
"%Y-%m-%d %H:%M:%S"
),
)
await bot.send_message(chat_id=tg_id, text=response_message, reply_markup=keyboard, parse_mode="HTML")
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
else:
response_message = "<b>Ключ не найден.</b>"
await bot.send_message(chat_id=tg_id, text=response_message, parse_mode="HTML")
await bot.send_message(
chat_id=tg_id, text=response_message, parse_mode="HTML"
)
finally:
await conn.close()
except Exception as e:
await bot.send_message(chat_id=tg_id, text=f"<b>Ошибка при выборе плана:</b> {e}", parse_mode="HTML")
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при выборе плана:</b> {e}",
parse_mode="HTML",
)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('confirm_delete|'))
@router.callback_query(F.data.startswith("confirm_delete|"))
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
client_id = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email FROM keys WHERE client_id = $1', client_id)
record = await conn.fetchrow(
"SELECT email FROM keys WHERE client_id = $1", client_id
)
if record:
email = record['email']
email = record["email"]
response_message = "Ключ успешно удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await delete_key(client_id)
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
async def delete_key_from_servers():
try:
tasks = []
for server_id in SERVERS:
tasks.append(delete_key_from_server(server_id, client_id))
await asyncio.gather(*tasks)
except Exception as e:
@@ -387,16 +558,27 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(f"Ошибка при удалении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.edit_message_text(
f"Ошибка при удалении ключа: {e}",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
await callback_query.answer()
@@ -404,74 +586,110 @@ async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
async def delete_key_from_server(server_id, client_id):
"""Удаление ключа с сервера"""
try:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
success = await delete_client(session, server_id, client_id)
if not success:
logger.error(f"Ошибка удаления ключа {client_id} на сервере {server_id}")
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}")
logger.error(
f"Ошибка при удалении ключа {client_id} с сервера {server_id}: {e}"
)
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
@router.callback_query(lambda c: c.data.startswith('renew_plan|'))
@router.callback_query(F.data.startswith("renew_plan|"))
async def process_callback_renew_plan(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
plan, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2]
days_to_extend = 30 * int(plan)
plan, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
)
days_to_extend = 30 * int(plan)
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
pass
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id)
record = await conn.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1", client_id
)
if record:
email = record['email']
expiry_time = record['expiry_time']
email = record["email"]
expiry_time = record["expiry_time"]
current_time = datetime.utcnow().timestamp() * 1000
if expiry_time <= current_time:
new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000)
new_expiry_time = int(
current_time
+ timedelta(days=days_to_extend).total_seconds() * 1000
)
else:
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
new_expiry_time = int(
expiry_time
+ timedelta(days=days_to_extend).total_seconds() * 1000
)
cost = RENEWAL_PLANS[plan]['price']
cost = RENEWAL_PLANS[plan]["price"]
balance = await get_balance(tg_id)
if balance < cost:
replenish_button = types.InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [back_button]])
replenish_button = types.InlineKeyboardButton(
text="Пополнить баланс", callback_data="replenish_balance"
)
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[replenish_button], [back_button]]
)
await bot.send_message(tg_id, INSUFFICIENT_FUNDS_MSG, reply_markup=keyboard, parse_mode="HTML")
await bot.send_message(
tg_id,
INSUFFICIENT_FUNDS_MSG,
reply_markup=keyboard,
parse_mode="HTML",
)
return
response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]['months'])
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile')
response_message = SUCCESS_RENEWAL_MSG.format(
months=RENEWAL_PLANS[plan]["months"]
)
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.send_message(tg_id, response_message, reply_markup=keyboard, parse_mode="HTML")
await bot.send_message(
tg_id, response_message, reply_markup=keyboard, parse_mode="HTML"
)
async def renew_key_on_servers():
tasks = []
for server_id in SERVERS:
task = asyncio.create_task(
renew_server_key(server_id, tg_id, client_id, email, new_expiry_time)
renew_server_key(
server_id, tg_id, client_id, email, new_expiry_time
)
)
tasks.append(task)
@@ -489,27 +707,37 @@ async def process_callback_renew_plan(callback_query: types.CallbackQuery):
await conn.close()
except Exception as e:
await bot.send_message(tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML")
await bot.send_message(
tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML"
)
await callback_query.answer()
async def renew_server_key(server_id, tg_id, client_id, email, new_expiry_time):
try:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
await extend_client_key(
session, server_id, tg_id, client_id, email, new_expiry_time
)
except Exception as e:
logger.error(f"Не удалось продлить ключ {client_id} на сервере {server_id}: {e}")
logger.error(
f"Не удалось продлить ключ {client_id} на сервере {server_id}: {e}"
)
async def handle_error(tg_id, callback_query, message):
try:
try:
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
pass
await bot.send_message(tg_id, message, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка при обработке ошибки: {e}")
+28 -25
View File
@@ -1,14 +1,11 @@
import base64
import logging
import aiohttp
from aiohttp import web
from loguru import logger
from config import SERVERS
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
async def fetch_url_content(url):
try:
@@ -18,54 +15,60 @@ async def fetch_url_content(url):
if response.status == 200:
content = await response.text()
logger.debug(f"Успешно получен контент с {url}")
return base64.b64decode(content).decode('utf-8').split("\n")
return base64.b64decode(content).decode("utf-8").split("\n")
else:
logger.error(f"Не удалось получить {url}, статус: {response.status}")
return []
logger.error(
f"Не удалось получить {url}, статус: {response.status}"
)
return []
except Exception as e:
logger.error(f"Ошибка при получении {url}: {e}")
return []
return []
async def combine_unique_lines(urls, query_string):
all_lines = []
logger.debug(f"Начинаем объединение подписок для запроса: {query_string}")
urls_with_query = [f"{url}?{query_string}" for url in urls]
logger.debug(f"Составлены URL-адреса: {urls_with_query}")
for url in urls_with_query:
lines = await fetch_url_content(url)
all_lines.extend(lines)
all_lines = list(set(filter(None, all_lines)))
logger.debug(f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов")
all_lines = list(set(filter(None, all_lines)))
logger.debug(
f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов"
)
return all_lines
async def handle_subscription(request):
email = request.match_info['email']
email = request.match_info["email"]
logger.info(f"Получен запрос на подписку для email: {email}")
urls = []
for server in SERVERS.values():
server_subscription_url = f"{server['SUBSCRIPTION']}/{email}"
urls.append(server_subscription_url)
query_string = request.query_string
logger.debug(f"Извлечен query string: {query_string}")
combined_subscriptions = await combine_unique_lines(urls, query_string)
base64_encoded = base64.b64encode("\n".join(combined_subscriptions).encode('utf-8')).decode('utf-8')
base64_encoded = base64.b64encode(
"\n".join(combined_subscriptions).encode("utf-8")
).decode("utf-8")
headers = {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Disposition': 'inline',
'profile-update-interval': '7',
'profile-title': email,
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": email,
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
return web.Response(text=base64_encoded, headers=headers)
return web.Response(text=base64_encoded, headers=headers)
+44 -22
View File
@@ -3,11 +3,11 @@ import uuid
from datetime import datetime, timedelta
import asyncpg
from loguru import logger
from auth import login_with_credentials
from client import add_client
from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, PUBLIC_LINK,
SERVERS)
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, PUBLIC_LINK, SERVERS
from database import store_key
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email
@@ -21,20 +21,22 @@ async def create_trial_key(tg_id: int):
public_link = f"{PUBLIC_LINK}{email}"
instructions = INSTRUCTIONS
result = {
'key': public_link,
'instructions': instructions
}
asyncio.create_task(generate_and_store_keys(tg_id, client_id, email, public_link))
result = {"key": public_link, "instructions": instructions}
asyncio.create_task(
generate_and_store_keys(tg_id, client_id, email, public_link)
)
return result
finally:
await conn.close()
async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public_link: str):
async def generate_and_store_keys(
tg_id: int, client_id: str, email: str, public_link: str
):
conn = await asyncpg.connect(DATABASE_URL)
try:
current_time = datetime.utcnow()
@@ -43,34 +45,54 @@ async def generate_and_store_keys(tg_id: int, client_id: str, email: str, public
tasks = []
for server_id in SERVERS:
task = create_key_on_server(server_id, client_id, email, tg_id, expiry_timestamp)
task = create_key_on_server(
server_id, client_id, email, tg_id, expiry_timestamp
)
tasks.append(task)
results = await asyncio.gather(*tasks)
if all(result.get("success") for result in results):
await store_key(tg_id, client_id, email, expiry_timestamp, public_link, server_id="all_servers")
await store_key(
tg_id,
client_id,
email,
expiry_timestamp,
public_link,
server_id="all_servers",
)
await conn.execute('''
await conn.execute(
"""
INSERT INTO connections (tg_id, trial)
VALUES ($1, 1)
ON CONFLICT (tg_id)
DO UPDATE SET trial = 1
''', tg_id)
""",
tg_id,
)
else:
print('Не удалось создать ключ на одном или нескольких серверах.')
logger.error("Не удалось создать ключ на одном или нескольких серверах.")
finally:
await conn.close()
async def create_key_on_server(server_id: str, client_id: str, email: str, tg_id: int, expiry_timestamp: int):
async def create_key_on_server(
server_id: str, client_id: str, email: str, tg_id: int, expiry_timestamp: int
):
"""Асинхронно создает ключ на указанном сервере и возвращает результат."""
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
response = await add_client(
session, server_id, client_id, email, tg_id,
limit_ip=1, total_gb=0, expiry_time=expiry_timestamp,
enable=True, flow="xtls-rprx-vision"
session,
server_id,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
)
return response
+209 -88
View File
@@ -1,43 +1,46 @@
import asyncio
import logging
from datetime import datetime, timedelta
import asyncpg
from aiogram import Bot, Router, types
from aiogram.fsm.state import State, StatesGroup
from loguru import logger
from auth import login_with_credentials
from client import delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS
from database import delete_key, get_balance, update_balance, update_key_expiry
from handlers.texts import (KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWAL_FAILED,
KEY_RENEWED)
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
from handlers.texts import (
KEY_EXPIRY_10H,
KEY_EXPIRY_24H,
KEY_RENEWAL_FAILED,
KEY_RENEWED,
RENEWAL_PLANS,
)
router = Router()
class NotificationStates(StatesGroup):
waiting_for_notification_text = State()
async def notify_expiring_keys(bot: Bot):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info("Подключение к базе данных успешно.")
current_time = datetime.utcnow().timestamp() * 1000
threshold_time_10h = (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000
threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
threshold_time_10h = (
datetime.utcnow() + timedelta(hours=10)
).timestamp() * 1000
threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
logger.info("Начало обработки уведомлений.")
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(1)
await asyncio.sleep(1)
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
await asyncio.sleep(1)
await asyncio.sleep(1)
await handle_expired_keys(bot, conn, current_time)
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Ошибка при отправке уведомлений: {e}")
@@ -46,25 +49,41 @@ async def notify_expiring_keys(bot: Bot):
await conn.close()
logger.info("Соединение с базой данных закрыто.")
async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
try:
# Проверка статуса бота в чате с пользователем
member = await bot.get_chat_member(chat_id, bot.id)
return member.status == 'left'
blocked = member.status == "left"
logger.info(
f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}"
)
return blocked
except Exception as e:
logger.error(f"Ошибка при проверке статуса бота у пользователя {chat_id}: {e}")
return False
# Обработка ошибок при проверке статуса
logger.warning(
f"Не удалось проверить статус бота для пользователя {chat_id}: {e}"
)
return False
async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float):
records = await conn.fetch('''
async def notify_10h_keys(
bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float
):
records = await conn.fetch(
"""
SELECT tg_id, email, expiry_time, client_id, server_id FROM keys
WHERE expiry_time <= $1 AND expiry_time > $2 AND notified = FALSE
''', threshold_time_10h, current_time)
""",
threshold_time_10h,
current_time,
)
logger.info(f"Найдено {len(records)} ключей для уведомления за 10 часов.")
for record in records:
tg_id = record['tg_id']
email = record['email']
expiry_time = record['expiry_time']
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
@@ -79,42 +98,70 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
days_left_message = f"{hours_left}"
message = KEY_EXPIRY_10H.format(
email=email,
expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S'),
days_left_message=days_left_message
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
)
if not await is_bot_blocked(bot, tg_id):
try:
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='🔄 Продлить VPN', callback_data=f'renew_key|{record["client_id"]}')],
[types.InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')],
[types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')]
])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="replenish_balance",
)
],
[
types.InlineKeyboardButton(
text="👤 Мой профиль", callback_data="view_profile"
)
],
]
)
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
continue
logger.error(
f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
)
continue
await conn.execute('UPDATE keys SET notified = TRUE WHERE client_id = $1', record['client_id'])
await conn.execute(
"UPDATE keys SET notified = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(f"Обновлено поле notified для клиента {record['client_id']}.")
await asyncio.sleep(1)
async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float):
await asyncio.sleep(1)
async def notify_24h_keys(
bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float
):
logger.info("Проверка истекших ключей...")
records_24h = await conn.fetch('''
records_24h = await conn.fetch(
"""
SELECT tg_id, email, expiry_time, client_id, server_id FROM keys
WHERE expiry_time <= $1 AND expiry_time > $2 AND notified_24h = FALSE
''', threshold_time_24h, current_time)
""",
threshold_time_24h,
current_time,
)
logger.info(f"Найдено {len(records_24h)} ключей для уведомления за 24 часа.")
for record in records_24h:
tg_id = record['tg_id']
email = record['email']
expiry_time = record['expiry_time']
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
@@ -131,26 +178,49 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
message_24h = KEY_EXPIRY_24H.format(
email=email,
days_left_message=days_left_message,
expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S')
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
if not await is_bot_blocked(bot, tg_id):
try:
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='🔄 Продлить VPN', callback_data=f'renew_key|{record["client_id"]}')],
[types.InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')],
[types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')]
])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="replenish_balance",
)
],
[
types.InlineKeyboardButton(
text="👤 Мой профиль", callback_data="view_profile"
)
],
]
)
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}")
continue
logger.error(
f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}"
)
continue
await conn.execute('UPDATE keys SET notified_24h = TRUE WHERE client_id = $1', record['client_id'])
logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.")
await asyncio.sleep(1)
await conn.execute(
"UPDATE keys SET notified_24h = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(
f"Обновлено поле notified_24h для клиента {record['client_id']}."
)
await asyncio.sleep(1)
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
@@ -159,26 +229,33 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
current_time = datetime.utcnow().timestamp() * 1000
adjusted_current_time = current_time + (3 * 60 * 60 * 1000)
logger.info(f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}")
logger.info(
f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}"
)
expiring_keys = await conn.fetch('''
expiring_keys = await conn.fetch(
"""
SELECT tg_id, client_id, expiry_time, email FROM keys
WHERE expiry_time <= $1
''', adjusted_current_time)
""",
adjusted_current_time,
)
logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.")
for record in expiring_keys:
tg_id = record['tg_id']
client_id = record['client_id']
email = record['email']
tg_id = record["tg_id"]
client_id = record["client_id"]
email = record["email"]
balance = await get_balance(tg_id)
expiry_time = record['expiry_time']
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
logger.info(f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}.")
logger.info(
f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}."
)
if time_left.total_seconds() <= 0:
days_left_message = "Ключ истек"
@@ -189,70 +266,114 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
message_expired = f"Ваш ключ {email} истек и был удален!\n\n Перейдите в профиль для создания нового ключа"
button_profile = types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
button_profile = types.InlineKeyboardButton(
text="👤 Мой профиль", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[button_profile]])
if balance >= 100:
await update_balance(tg_id, -100)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
if balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(client_id, new_expiry_time)
logger.info(f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}.")
logger.info(
f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}."
)
all_success = True
for server_id in SERVERS:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
success = await extend_client_key(
session, server_id, tg_id, client_id, email, new_expiry_time
)
if not success:
all_success = False
logger.error(f"Не удалось продлить ключ для пользователя {tg_id} на сервере {server_id}.")
logger.error(
f"Не удалось продлить ключ для пользователя {tg_id} на сервере {server_id}."
)
if all_success:
try:
await bot.send_message(tg_id, KEY_RENEWED, reply_markup=keyboard)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен на месяц на всех серверах.")
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен на месяц на всех серверах."
)
except Exception as e:
if 'blocked' in str(e).lower():
logger.warning(f"Пользователь {tg_id} заблокирован. Ключ будет удален.")
if "blocked" in str(e).lower():
logger.warning(
f"Пользователь {tg_id} заблокирован. Ключ будет удален."
)
await delete_key(client_id)
for server_id in SERVERS:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
success = await delete_client(session, server_id, client_id)
if success:
logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.")
logger.info(
f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}."
)
else:
logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.")
logger.error(
f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}."
)
else:
logger.error(f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}")
logger.error(
f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}"
)
else:
try:
await bot.send_message(tg_id, KEY_RENEWAL_FAILED, reply_markup=keyboard)
logger.error(f"Не удалось продлить ключ для пользователя {tg_id} на одном или нескольких серверах.")
await bot.send_message(
tg_id, KEY_RENEWAL_FAILED, reply_markup=keyboard
)
logger.error(
f"Не удалось продлить ключ для пользователя {tg_id} на одном или нескольких серверах."
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления о неудачном продлении ключа пользователю {tg_id}: {e}")
logger.error(
f"Ошибка при отправке уведомления о неудачном продлении ключа пользователю {tg_id}: {e}"
)
else:
try:
await bot.send_message(tg_id, message_expired, reply_markup=keyboard)
await delete_key(client_id)
for server_id in SERVERS:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
success = await delete_client(session, server_id, client_id)
if success:
logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.")
logger.info(
f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}."
)
else:
logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.")
logger.error(
f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}."
)
except Exception as e:
if 'blocked' in str(e).lower():
logger.warning(f"Пользователь {tg_id} заблокирован. Ключ будет удален.")
if "blocked" in str(e).lower():
logger.warning(
f"Пользователь {tg_id} заблокирован. Ключ будет удален."
)
await delete_key(client_id)
for server_id in SERVERS:
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
session = await login_with_credentials(
server_id, ADMIN_USERNAME, ADMIN_PASSWORD
)
success = await delete_client(session, server_id, client_id)
if success:
logger.info(f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}.")
logger.info(
f"Ключ для клиента {tg_id} успешно удален с сервера {server_id}."
)
else:
logger.error(f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}.")
logger.error(
f"Не удалось удалить ключ для клиента {tg_id} на сервере {server_id}."
)
else:
logger.error(f"Ошибка при удалении ключа для клиента {tg_id}: {e}")
await asyncio.sleep(1)
await asyncio.sleep(1)
@@ -1,31 +1,35 @@
import hashlib
import logging
import time
import uuid
import requests
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiohttp import web
from loguru import logger
from bot import bot
from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID
from database import update_balance
from handlers.profile import process_callback_view_profile
from handlers.texts import PAYMENT_OPTIONS
router = Router()
logging.basicConfig(level=logging.DEBUG)
class ReplenishBalanceState(StatesGroup):
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
def generate_signature(params, api_key):
sign_string = ":".join([str(params[k]) for k in sorted(params)]) + api_key
return hashlib.md5(sign_string.encode()).hexdigest()
async def create_payment(user_id, amount, email, ip):
payment_id = str(uuid.uuid4())
nonce = int(time.time() * 1000)
@@ -36,41 +40,45 @@ async def create_payment(user_id, amount, email, ip):
"paymentId": payment_id,
"email": email,
"ip": ip,
"i": 6,
"nonce": nonce
"i": 6,
"nonce": nonce,
}
params["signature"] = generate_signature(params, FREEKASSA_API_KEY)
try:
response = requests.post("https://api.freekassa.com/v1/orders/create", json=params)
response = requests.post(
"https://api.freekassa.com/v1/orders/create", json=params
)
response_data = response.json()
logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}")
logger.debug(f"Ответ от FreeKassa при создании платежа: {response_data}")
if response_data.get("type") == "success":
return response_data["location"]
return response_data["location"]
else:
logging.error(f"Ошибка создания платежа: {response_data}")
logger.error(f"Ошибка создания платежа: {response_data}")
return None
except Exception as e:
logging.error(f"Ошибка запроса к FreeKassa: {e}")
logger.error(f"Ошибка запроса к FreeKassa: {e}")
return None
async def send_payment_success_notification(user_id, amount):
try:
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!"
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def freekassa_webhook(request):
data = await request.json()
logging.debug(f"Получен вебхук от FreeKassa: {data}")
logger.debug(f"Получен вебхук от FreeKassa: {data}")
logging.debug(f"Данные вебхука от FreeKassa: {data}")
logger.debug(f"Данные вебхука от FreeKassa: {data}")
if data["status"] == "completed":
user_id = data["metadata"]["user_id"]
@@ -81,38 +89,49 @@ async def freekassa_webhook(request):
return web.Response(status=200)
@router.callback_query(lambda c: c.data == 'pay_freekassa')
async def process_callback_pay_freekassa(callback_query: types.CallbackQuery, state: FSMContext):
@router.callback_query(F.data == "pay_freekassa")
async def process_callback_pay_freekassa(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="100 рублей", callback_data="amount|100"),
InlineKeyboardButton(text="500 рублей", callback_data="amount|500")
],
[
InlineKeyboardButton(text="1000 рублей", callback_data="amount|1000"),
InlineKeyboardButton(text="5000 рублей", callback_data="amount|5000")
],
[
InlineKeyboardButton(text="Введите другую сумму", callback_data="enter_custom_amount")
]
])
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
inline_keyboard = []
for payment in PAYMENT_OPTIONS:
inline_keyboard.append(
InlineKeyboardButton(
text=payment.get("text"), callback_data=payment.get("callback_data")
)
)
amount_keyboard = InlineKeyboardMarkup(inline_keyboard)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения через FreeKassa:",
reply_markup=amount_keyboard
reply_markup=amount_keyboard,
)
await state.set_state(ReplenishBalanceState.choosing_amount)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('amount|'))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
data = callback_query.data.split('|', 1)
@router.callback_query(F.data == "back_to_profile")
async def back_to_profile_handler(
callback_query: types.CallbackQuery, state: FSMContext
):
await process_callback_view_profile(callback_query, state)
@router.callback_query(F.data.startswith("amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
amount_str = data[1]
try:
amount = int(amount_str)
@@ -120,39 +139,55 @@ async def process_amount_selection(callback_query: types.CallbackQuery, state: F
await bot.send_message(callback_query.from_user.id, "Некорректная сумма.")
return
user_email = f"{callback_query.from_user.id}@solo.net"
user_ip = callback_query.message.chat.id
payment_url = await create_payment(callback_query.from_user.id, amount, user_email, user_ip)
user_email = f"{callback_query.from_user.id}@solo.net"
user_ip = callback_query.message.chat.id
payment_url = await create_payment(
callback_query.from_user.id, amount, user_email, user_ip
)
if payment_url:
await bot.send_message(callback_query.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}")
await bot.send_message(
callback_query.from_user.id,
f"Перейдите по ссылке для оплаты: {payment_url}",
)
else:
await bot.send_message(callback_query.from_user.id, "Ошибка при создании платежа. Попробуйте позже.")
await bot.send_message(
callback_query.from_user.id,
"Ошибка при создании платежа. Попробуйте позже.",
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'enter_custom_amount')
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(
text="Введите сумму пополнения:"
)
@router.callback_query(F.data == "enter_custom_amount")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:")
await message.answer(
"Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:"
)
return
user_email = f"{message.from_user.id}@solo.net"
user_ip = message.chat.id
payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip)
payment_url = await create_payment(
message.from_user.id, amount, user_email, user_ip
)
if payment_url:
await bot.send_message(message.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}")
await bot.send_message(
message.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}"
)
else:
await message.answer("Ошибка при создании платежа. Попробуйте позже.")
-280
View File
@@ -1,280 +0,0 @@
import logging
import uuid
from aiogram import Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiohttp import web
from yookassa import Configuration, Payment
from bot import bot
from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID
from database import (add_connection, check_connection_exists, get_key_count,
update_balance)
from handlers.profile import process_callback_view_profile
from handlers.texts import PAYMENT_OPTIONS
router = Router()
logging.basicConfig(level=logging.DEBUG)
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
logging.debug(f"Account ID: {YOOKASSA_SHOP_ID}")
logging.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}")
class ReplenishBalanceState(StatesGroup):
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key='last_message_id'):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(chat_id=chat_id, message_id=previous_message_id)
sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
print(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(lambda c: c.data == 'replenish_balance')
async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[0]['text'], callback_data=PAYMENT_OPTIONS[0]['callback_data']),
InlineKeyboardButton(text=PAYMENT_OPTIONS[1]['text'], callback_data=PAYMENT_OPTIONS[1]['callback_data'])
],
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[2]['text'], callback_data=PAYMENT_OPTIONS[2]['callback_data']),
InlineKeyboardButton(text=PAYMENT_OPTIONS[3]['text'], callback_data=PAYMENT_OPTIONS[3]['callback_data'])
],
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[4]['text'], callback_data=PAYMENT_OPTIONS[4]['callback_data'])
],
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[5]['text'], callback_data=PAYMENT_OPTIONS[5]['callback_data'])
],
])
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=amount_keyboard
)
await state.set_state(ReplenishBalanceState.choosing_amount)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'back_to_profile')
async def back_to_profile_handler(callback_query: types.CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
@router.callback_query(lambda c: c.data.startswith('amount_'))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
data = callback_query.data.split('_', 1)
if len(data) != 2:
await send_message_with_deletion(callback_query.from_user.id, "Неверные данные для выбора суммы.", state=state, message_key='amount_error_message_id')
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await send_message_with_deletion(callback_query.from_user.id, "Некорректная сумма.", state=state, message_key='amount_error_message_id')
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
state_data = await state.get_data()
customer_name = callback_query.from_user.full_name
customer_id = callback_query.from_user.id
customer_email = f"{customer_id}@solo.net"
payment = Payment.create({
"amount": {
"value": str(amount),
"currency": "RUB"
},
"confirmation": {
"type": "redirect",
"return_url": "https://pocomacho.ru/"
},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": customer_name,
"email": customer_email,
"phone": "79000000000"
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {
"value": str(amount),
"currency": "RUB"
},
"vat_code": 6
}
]
},
"metadata": {
"user_id": customer_id
}
}, uuid.uuid4())
if payment['status'] == 'pending':
payment_url = payment['confirmation']['confirmation_url']
confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Пополнить', url=payment_url)],
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')]
])
await callback_query.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard
)
else:
await send_message_with_deletion(callback_query.from_user.id, "Ошибка при создании платежа.", state=state)
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
profile_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')]
])
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=profile_keyboard
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def payment_webhook(request):
event = await request.json()
logging.debug(f"Webhook event received: {event}")
if event['event'] == 'payment.succeeded':
user_id_str = event['object']['metadata']['user_id']
amount_str = event['object']['amount']['value']
try:
user_id = int(user_id_str)
amount = float(amount_str)
logging.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logging.error(f"Ошибка конвертации user_id или amount: {e}")
return web.Response(status=400)
return web.Response(status=200)
@router.callback_query(lambda c: c.data == 'enter_custom_amount')
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(
text="Введите сумму пополнения:"
)
await state.set_state(ReplenishBalanceState.entering_custom_amount)
await callback_query.answer()
@router.message(State(ReplenishBalanceState.entering_custom_amount))
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:")
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
try:
payment = Payment.create({
"amount": {
"value": str(amount),
"currency": "RUB"
},
"confirmation": {
"type": "redirect",
"return_url": "https://pocomacho.ru/"
},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": message.from_user.full_name,
"email": f"{message.from_user.id}@solo.net",
"phone": "79000000000"
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {
"value": str(amount),
"currency": "RUB"
},
"vat_code": 6
}
]
},
"metadata": {
"user_id": message.from_user.id
}
}, uuid.uuid4())
if payment['status'] == 'pending':
payment_url = payment['confirmation']['confirmation_url']
confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Пополнить', url=payment_url)],
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')]
])
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard
)
else:
await message.answer("Ошибка при создании платежа.")
except Exception as e:
logging.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
+312
View File
@@ -0,0 +1,312 @@
import uuid
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiohttp import web
from loguru import logger
from yookassa import Configuration, Payment
from bot import bot
from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID
from database import (
add_connection,
check_connection_exists,
get_key_count,
update_balance,
)
from handlers.profile import process_callback_view_profile
from handlers.texts import PAYMENT_OPTIONS
router = Router()
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
if YOOKASSA_ENABLE:
logger.debug(f"Account ID: {YOOKASSA_SHOP_ID}")
logger.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}")
class ReplenishBalanceState(StatesGroup):
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "replenish_balance")
async def process_callback_replenish_balance(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
inline_keyboard = []
for payment in PAYMENT_OPTIONS:
inline_keyboard.append(
InlineKeyboardButton(
text=payment.get("text"), callback_data=payment.get("callback_data")
)
)
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
amount_keyboard = InlineKeyboardMarkup(inline_keyboard)
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.send_message(
chat_id=tg_id, text="Выберите сумму пополнения:", reply_markup=amount_keyboard
)
await state.set_state(ReplenishBalanceState.choosing_amount)
await callback_query.answer()
@router.callback_query(F.data == "back_to_profile")
async def back_to_profile_handler(
callback_query: types.CallbackQuery, state: FSMContext
):
await process_callback_view_profile(callback_query, state)
@router.callback_query(F.data.startswith("amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
await send_message_with_deletion(
callback_query.from_user.id,
"Неверные данные для выбора суммы.",
state=state,
message_key="amount_error_message_id",
)
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await send_message_with_deletion(
callback_query.from_user.id,
"Некорректная сумма.",
state=state,
message_key="amount_error_message_id",
)
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
state_data = await state.get_data()
customer_name = callback_query.from_user.full_name
customer_id = callback_query.from_user.id
customer_email = f"{customer_id}@solo.net"
payment = Payment.create(
{
"amount": {"value": str(amount), "currency": "RUB"},
"confirmation": {"type": "redirect", "return_url": "https://pocomacho.ru/"},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": customer_name,
"email": customer_email,
"phone": "79000000000",
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {"value": str(amount), "currency": "RUB"},
"vat_code": 6,
}
],
},
"metadata": {"user_id": customer_id},
},
uuid.uuid4(),
)
if payment["status"] == "pending":
payment_url = payment["confirmation"]["confirmation_url"]
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Пополнить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile")],
]
)
await callback_query.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard,
)
else:
await send_message_with_deletion(
callback_query.from_user.id, "Ошибка при создании платежа.", state=state
)
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
profile_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
]
]
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=profile_keyboard,
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def yookassa_webhook(request):
event = await request.json()
logger.debug(f"Webhook event received: {event}")
if event["event"] == "payment.succeeded":
user_id_str = event["object"]["metadata"]["user_id"]
amount_str = event["object"]["amount"]["value"]
try:
user_id = int(user_id_str)
amount = float(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
return web.Response(status=400)
return web.Response(status=200)
@router.callback_query(F.data == "enter_custom_amount")
async def process_enter_custom_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(text="Введите сумму пополнения:")
await state.set_state(ReplenishBalanceState.entering_custom_amount)
await callback_query.answer()
@router.message(State(ReplenishBalanceState.entering_custom_amount))
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer(
"Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
try:
payment = Payment.create(
{
"amount": {"value": str(amount), "currency": "RUB"},
"confirmation": {
"type": "redirect",
"return_url": "https://pocomacho.ru/",
},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": message.from_user.full_name,
"email": f"{message.from_user.id}@solo.net",
"phone": "79000000000",
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {"value": str(amount), "currency": "RUB"},
"vat_code": 6,
}
],
},
"metadata": {"user_id": message.from_user.id},
},
uuid.uuid4(),
)
if payment["status"] == "pending":
payment_url = payment["confirmation"]["confirmation_url"]
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Пополнить", url=payment_url)],
[
InlineKeyboardButton(
text="⬅️ Назад", callback_data="back_to_profile"
)
],
]
)
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard,
)
else:
await message.answer("Ошибка при создании платежа.")
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
+78 -64
View File
@@ -1,105 +1,119 @@
import logging
import os
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (BufferedInputFile, InlineKeyboardButton,
InlineKeyboardMarkup)
from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from loguru import logger
from bot import bot
from config import PAYMENT_METHOD
from config import CHANNEL_URL, FREEKASSA_ENABLE, PAYMENT_METHOD, YOOKASSA_ENABLE
from database import get_balance, get_key_count, get_referral_stats
from handlers.texts import (CHANNEL_LINK, get_referral_link,
invite_message_send, profile_message_send)
class ReplenishBalanceState(StatesGroup):
choosing_transfer_method = State()
waiting_for_admin_confirmation = State()
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
router = Router()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
username = callback_query.from_user.full_name
async def process_callback_view_profile(
callback_query: types.CallbackQuery, state: FSMContext
):
chat_id = callback_query.from_user.id
username = callback_query.from_user.full_name
image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg')
if not os.path.isfile(image_path):
await bot.send_message(tg_id, "Файл изображения не найден.")
return
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
try:
key_count = await get_key_count(tg_id)
balance = await get_balance(tg_id)
key_count = await get_key_count(chat_id)
balance = await get_balance(chat_id)
if balance is None:
balance = 0
balance = 0
profile_message = profile_message_send(username, chat_id, balance, key_count)
profile_message = profile_message_send(username, tg_id, balance, key_count)
profile_message += (
f"<b>Обязательно подпишитесь на канал</b> <a href='{CHANNEL_LINK}'>здесь</a>\n"
)
if key_count == 0:
profile_message += "\n<i>Нажмите ➕Устройство снизу, чтобы добавить устройство в VPN</i>"
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=' Устройство', callback_data='create_key'), InlineKeyboardButton(text='📱 Мои устр-ва', callback_data='view_keys')],
[InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='pay_freekassa' if PAYMENT_METHOD == 'freekassa' else 'replenish_balance')],
[InlineKeyboardButton(text='👥 Пригласить', callback_data='invite'), InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions')],
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')]
])
profile_message += (
"\n<i>Нажмите ➕Устройство снизу, чтобы добавить устройство в VPN</i>"
)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL))
builder.row(
InlineKeyboardButton(text=" Устройство", callback_data="create_key"),
InlineKeyboardButton(text="📱 Мои устр-ва", callback_data="view_keys"),
)
if YOOKASSA_ENABLE or FREEKASSA_ENABLE:
builder.row(
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data=(
"pay_freekassa"
if PAYMENT_METHOD == "freekassa"
else "replenish_balance"
),
)
)
builder.row(
InlineKeyboardButton(text="👥 Пригласить", callback_data="invite"),
InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"),
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))
# Попробуем удалить предыдущее сообщение
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}") # Логируем ошибку, если удаление не удалось
logger.error(
f"Ошибка при удалении сообщения: {e}"
) # Логируем ошибку, если удаление не удалось
with open(image_path, 'rb') as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
parse_mode='HTML',
reply_markup=inline_keyboard
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(tg_id, f"❗️ Ошибка при получении данных профиля: {e}")
await bot.send_message(chat_id, f"❗️ Ошибка при получении данных профиля: {e}")
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'invite')
@router.callback_query(F.data == "invite")
async def invite_handler(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
referral_link = get_referral_link(tg_id)
referral_stats = await get_referral_stats(tg_id)
invite_message = (
invite_message_send(referral_link,referral_stats)
)
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='view_profile')
keyboard = InlineKeyboardMarkup(inline_keyboard=[[button_back]])
chat_id = callback_query.from_user.id
referral_link = get_referral_link(chat_id)
referral_stats = await get_referral_stats(chat_id)
invite_message = invite_message_send(referral_link, referral_stats)
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
await callback_query.message.delete()
await bot.send_message(
chat_id=tg_id,
chat_id=chat_id,
text=invite_message,
parse_mode='HTML',
reply_markup=keyboard
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'view_profile')
@router.callback_query(F.data == "view_profile")
async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
+88 -77
View File
@@ -1,87 +1,102 @@
import os
import asyncpg
from aiogram import Router
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (BufferedInputFile, CallbackQuery,
InlineKeyboardButton, InlineKeyboardMarkup, Message)
from aiogram.types import (
BufferedInputFile,
CallbackQuery,
InlineKeyboardButton,
InlineKeyboardMarkup,
Message,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
from loguru import logger
from bot import bot
from config import APP_URL, CHANNEL_URL, DATABASE_URL, SUPPORT_CHAT_URL
from database import (add_connection, add_referral, check_connection_exists,
get_trial)
from database import add_connection, add_referral, check_connection_exists, get_trial
from handlers.keys.trial_key import create_trial_key
from handlers.texts import ABOUT_VPN, INSTRUCTIONS_TRIAL, WELCOME_TEXT
router = Router()
class FeedbackState(StatesGroup):
waiting_for_feedback = State()
async def send_welcome_message(chat_id: int, trial_status: int):
welcome_text = WELCOME_TEXT
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg')
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(
InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")
)
builder.row(
InlineKeyboardButton(text="👤 Мой профиль", callback_data="view_profile")
)
builder.row(
InlineKeyboardButton(text="📞 Поддержка", url=SUPPORT_CHAT_URL),
InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL),
)
builder.row(InlineKeyboardButton(text="🔒 О VPN", callback_data="about_vpn"))
if not os.path.isfile(image_path):
await bot.send_message(chat_id, "Файл изображения не найден.")
return
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='🔗 Подключить VPN', callback_data='connect_vpn')] if trial_status == 0 else [],
[InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')],
[InlineKeyboardButton(text='🔒 О VPN', callback_data='about_vpn')],
[InlineKeyboardButton(text='📞 Поддержка', url=SUPPORT_CHAT_URL)],
[InlineKeyboardButton(text='📢 Наш канал', url=CHANNEL_URL)],
])
inline_keyboard.inline_keyboard = [row for row in inline_keyboard.inline_keyboard if row]
with open(image_path, 'rb') as image_from_buffer:
await bot.send_photo(
chat_id,
BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
caption=welcome_text,
parse_mode='HTML',
reply_markup=inline_keyboard
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
caption=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
@router.message(Command('start'))
@router.message(Command("start"))
async def start_command(message: Message):
print(f"Received start command with text: {message.text}")
if 'referral_' in message.text:
referrer_tg_id = int(message.text.split('referral_')[1])
print(f"Referral ID: {referrer_tg_id}")
logger.info(f"Received start command with text: {message.text}")
if "referral_" in message.text:
referrer_tg_id = int(message.text.split("referral_")[1])
logger.info(f"Referral ID: {referrer_tg_id}")
if not await check_connection_exists(message.from_user.id):
await add_connection(message.from_user.id)
await add_referral(message.from_user.id, referrer_tg_id)
await message.answer("Вас пригласил друг, добро пожаловать!")
else:
await message.answer("Вы уже зарегистрированы в системе!")
trial_status = await get_trial(message.from_user.id)
await send_welcome_message(message.chat.id, trial_status)
@router.callback_query(lambda c: c.data == 'connect_vpn')
@router.callback_query(F.data == "connect_vpn")
async def handle_connect_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
user_id = callback_query.from_user.id
trial_key_info = await create_trial_key(user_id)
if 'error' in trial_key_info:
await callback_query.message.answer(trial_key_info['error'])
if "error" in trial_key_info:
await callback_query.message.answer(trial_key_info["error"])
else:
conn = await asyncpg.connect(DATABASE_URL)
try:
result = await conn.execute('''
result = await conn.execute(
"""
UPDATE connections SET trial = 1 WHERE tg_id = $1
''', user_id)
print(f"Rows updated: {result}")
""",
user_id,
)
logger.info(f"Rows updated: {result}")
except Exception as e:
print(f"Ошибка при обновлении trial: {e}")
logger.error(f"Ошибка при обновлении trial: {e}")
finally:
await conn.close()
@@ -91,58 +106,54 @@ async def handle_connect_vpn(callback_query: CallbackQuery):
f"<b>Инструкции:</b>\n{INSTRUCTIONS_TRIAL}"
)
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
button_iphone = InlineKeyboardButton(
text='🍏 Подключить',
url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}'
)
button_android = InlineKeyboardButton(
text='🤖 Подключить',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}'
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Мой профиль", callback_data="view_profile")
)
button_download_iphone = InlineKeyboardButton(
text='🍏 Скачать',
url='https://apps.apple.com/ru/app/v2raytun/id6476628951'
)
button_download_android = InlineKeyboardButton(
text='🤖 Скачать',
url='https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru'
builder.row(
InlineKeyboardButton(
text="🍏 Подключить",
url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}',
),
InlineKeyboardButton(
text="🤖 Подключить",
url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}',
),
)
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[button_download_iphone, button_download_android],
[button_iphone, button_android],
[button_profile]
])
builder.row(
InlineKeyboardButton(
text="🍏 Скачать",
url="https://apps.apple.com/ru/app/v2raytun/id6476628951",
),
InlineKeyboardButton(
text="🤖 Скачать",
url="https://play.google.com/store/apps/details?id=com.v2raytun.android&hl=ru",
),
)
await callback_query.message.answer(
key_message,
parse_mode='HTML',
reply_markup=inline_keyboard
key_message, parse_mode="HTML", reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'about_vpn')
@router.callback_query(F.data == "about_vpn")
async def handle_about_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
bot_version = "3.0.1_beta"
info_message = ABOUT_VPN.format(bot_version=bot_version)
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')
inline_keyboard_back = InlineKeyboardMarkup(inline_keyboard=[[button_back]])
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))
await callback_query.message.answer(
info_message,
parse_mode='HTML',
reply_markup=inline_keyboard_back
ABOUT_VPN, parse_mode="HTML", reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'back_to_menu')
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
+11 -7
View File
@@ -5,23 +5,27 @@ from config import SERVERS
def sanitize_key_name(key_name: str) -> str:
return re.sub(r'[^a-z0-9@._-]', '', key_name.lower())
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
def generate_random_email():
"""Генерирует случайный набор символов."""
random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=6))
return random_string
random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6))
return random_string
async def get_least_loaded_server(conn):
"""Находит сервер с наименьшей загрузкой."""
least_loaded_server_id = None
min_load_percentage = float('inf')
min_load_percentage = float("inf")
for server_id, server in SERVERS.items():
count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id)
percent_full = (count / 60) * 100 if count <= 60 else 100
count = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE server_id = $1", server_id
)
percent_full = (count / 60) * 100 if count <= 60 else 100
if percent_full < min_load_percentage:
min_load_percentage = percent_full
least_loaded_server_id = server_id
return least_loaded_server_id
return least_loaded_server_id
+29
View File
@@ -0,0 +1,29 @@
from functools import wraps
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from config import ADMIN_ID
class AdminMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
user_id = None
if isinstance(event, Message):
user_id = event.from_user.id
elif isinstance(event, CallbackQuery):
user_id = event.from_user.id
if user_id != int(ADMIN_ID):
data["is_admin"] = False
else:
data["is_admin"] = True
return await handler(event, data)
+23
View File
@@ -0,0 +1,23 @@
from functools import wraps
from typing import Any, Awaitable, Callable, Dict
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from config import DATABASE_URL
class AdminMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
session = await asyncpg.connect(DATABASE_URL)
data["session"] = session
try:
return await handler(event, data)
finally:
await session.close()
+37
View File
@@ -0,0 +1,37 @@
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from loguru import logger
class UserActivityMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
user_id = None
username = None
action = None
if isinstance(event, Message):
user_id = event.from_user.id
username = event.from_user.username
action = f"Сообщение: {event.text}"
elif isinstance(event, CallbackQuery):
user_id = event.from_user.id
username = event.from_user.username
action = f"Обратный вызов: {event.data}"
# Логируем действие пользователя
logger.info(
f"Активность пользователя - "
f"ID пользователя: {user_id}, "
f"Имя пользователя: {username}, "
f"Действие: {action}"
)
# Продолжаем выполнение обработчика
return await handler(event, data)
+1
View File
@@ -25,3 +25,4 @@ urllib3==2.2.3
wrapt==1.16.0
yarl==1.15.5
yookassa==3.3.0
loguru