Refactoring and fix

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