126 Commits

Author SHA1 Message Date
Vladless 722661888a add version 2024-11-17 01:32:52 +03:00
Vladless b080387f67 robokassa release 2024-11-17 00:01:26 +03:00
Vladislav Lisitsyn be3c2aa73a Merge pull request #94 from izzzzzi/main
Add scripts
2024-11-16 22:54:50 +03:00
Zahar Izmailov d411ee5eff Merge branch 'Vladless:main' into main 2024-11-16 21:06:04 +03:00
Zakhar Izmaylov f5b01c7b49 Delete nginx config 2024-11-16 21:04:06 +03:00
Zakhar Izmaylov 086f73cf6d Update 2024-11-16 21:03:39 +03:00
Zakhar Izmaylov f996c16669 Add bash for node 2024-11-16 19:05:09 +03:00
Vladislav Lisitsyn e4e12c2dff Merge pull request #93 from izzzzzi/main
Add dry logging
2024-11-16 18:46:12 +03:00
Zakhar Izmaylov 8ba3195030 Add dry logging 2024-11-16 18:01:28 +03:00
Vladless e512ecae4f fix delete 2024-11-16 00:44:12 +03:00
Vladless 7eb89d7c23 ip 2024-11-15 23:59:42 +03:00
Vladless 8efafe206f fix back_button 2024-11-15 23:27:08 +03:00
Vladless 0317b14d2e Merge branch 'main' of https://github.com/Vladless/Solo_bot 2024-11-15 23:15:41 +03:00
Vladless 2a9c407439 fix logging 2024-11-15 23:15:19 +03:00
Vladislav Lisitsyn 2e6d3c8b12 Update start.py 2024-11-15 22:06:23 +03:00
Vladless 74bb638806 pc instructions and button 2024-11-15 21:49:27 +03:00
Vladless fe00b3534d fix cluster 2024-11-15 20:30:57 +03:00
Vladless 16b438c93f changing notifications 2024-11-15 20:15:19 +03:00
Vladless 304caf2242 logging for add client 2024-11-15 19:24:09 +03:00
Vladless 37d3eeb277 formatting 2024-11-15 19:19:13 +03:00
Vladless 3ee6449de0 rm DEBUG 2024-11-15 19:16:37 +03:00
Vladless 116a65e48e total_gb 2024-11-15 19:16:06 +03:00
Vladislav Lisitsyn e61ab751bb Merge pull request #92 from izzzzzi/main
Fix pay stars
2024-11-15 18:03:04 +03:00
Zakhar Izmaylov da99e358df Fix router 2024-11-15 17:28:54 +03:00
Zakhar Izmaylov b9e9eeb214 Fix pay stars 2024-11-15 17:28:06 +03:00
Zakhar Izmaylov 124ec36661 Fix pay stars 2024-11-15 17:25:42 +03:00
Vladless f29fd41a4c fix donate 2024-11-15 15:48:52 +03:00
Vladless 098b879c5c fix donate 2024-11-15 15:40:20 +03:00
Vladislav Lisitsyn 24abd4c087 Merge pull request #91 from izzzzzi/main
Add donate
2024-11-15 15:09:46 +03:00
Vladless 85e5d96112 Merge branch 'main' of https://github.com/Vladless/Solo_bot 2024-11-15 15:09:01 +03:00
Vladless 1a85ff4ae8 robokassa_beta 2024-11-15 15:07:51 +03:00
Zakhar Izmaylov e87df0c8eb Add donate 2024-11-15 14:38:14 +03:00
Vladislav Lisitsyn 213284f5f8 Merge pull request #90 from izzzzzi/main
Added support for the list of admins
2024-11-15 13:51:33 +03:00
Vladless 37110b230b fix message delete 2024-11-15 13:51:12 +03:00
Zahar Izmailov 8f12316584 Merge branch 'Vladless:main' into main 2024-11-15 13:37:57 +03:00
Zakhar Izmaylov 85d6fbc3e3 Added support for the list of admins 2024-11-15 13:37:24 +03:00
Vladislav Lisitsyn 317e591d1f Merge pull request #89 from izzzzzi/main
Fix stars
2024-11-15 13:30:46 +03:00
Zakhar Izmaylov 0f0b2708e7 Fix stars 2024-11-15 13:25:57 +03:00
Vladislav Lisitsyn fe6bbe7088 Merge pull request #88 from izzzzzi/main
Add dev mode and fix menu and fix stars and cryptobot
2024-11-15 12:53:05 +03:00
Zakhar Izmaylov bba036e7e9 Add multy pay 2024-11-15 11:58:48 +03:00
Zakhar Izmaylov 2f10ae0b74 Add dev mode and fix menu and fix stars and cryptobot 2024-11-15 11:40:25 +03:00
Vladless deafbfb4ee coupons initial 2024-11-15 03:03:24 +03:00
Vladless afe701ccaf fix payments 2024-11-14 15:13:52 +03:00
Vladless 9b8024b8c3 servers in clusters/total_gb 2024-11-14 14:17:16 +03:00
Vladless 8de8236703 servers in clusters/total_gb 2024-11-14 03:04:23 +03:00
Vladless 3fb4150a9e servers in clusters/total_gb 2024-11-14 03:03:32 +03:00
Vladless 5257621f43 custom trial time 2024-11-13 18:51:25 +03:00
Vladislav Lisitsyn 6ab6ac231a Merge pull request #87 from izzzzzi/main
Update keyboards and restructuring for filters
2024-11-13 18:04:38 +03:00
Vladless be51a4cdbf custom bonus 2024-11-13 17:27:08 +03:00
Zakhar Izmaylov 2bbebe7d4f Add models.py #TODO 2024-11-13 17:10:25 +03:00
Zakhar Izmaylov 0a2b164c49 Add DatabaseMiddleware #TODO 2024-11-13 17:03:18 +03:00
Zakhar Izmaylov 33c68527c2 Restructuring for filters 2024-11-13 16:53:23 +03:00
Zakhar Izmaylov 3a46d12657 Update keyboards, update texts.py 2024-11-13 16:48:56 +03:00
Vladless 399537530a add TOTAL_GB 2024-11-13 16:18:16 +03:00
Vladless f6aa28a0e7 small fix 2024-11-13 15:38:20 +03:00
Vladless a7e5e74857 small fix 2024-11-13 05:41:49 +03:00
Vladless 2f4caeeeb7 redesign/bug fixes/admin trial 2024-11-13 04:05:43 +03:00
Vladless 92af5e5d37 redesign/bug fixes/admin trial 2024-11-13 04:04:40 +03:00
Vladless 9b87d02b5d redesign/bug fixes/admin trial 2024-11-13 04:01:13 +03:00
Vladless 0f9d4e3935 redesign/bug fixes/admin trial 2024-11-13 03:59:34 +03:00
Vladislav Lisitsyn 23f668e251 Merge pull request #86 from izzzzzi/main
Refactoring admin
2024-11-12 13:11:09 +03:00
Zakhar Izmaylov fa27648c7c Refactoring user editor 2024-11-12 08:10:40 +03:00
Zakhar Izmaylov bc63a4b070 Refactoring 2024-11-12 08:10:28 +03:00
Zakhar Izmaylov 8c3e055acd Refactoring admin 2024-11-12 07:51:54 +03:00
Vladislav Lisitsyn 1b7b7b1e18 Merge pull request #85 from izzzzzi/main
Fix formating and freekassa
2024-11-12 00:17:33 +03:00
Zakhar Izmaylov c63c8d08cf Fix formating and freekassa 2024-11-12 00:11:33 +03:00
Vladless 840ef3648b Merge branch 'main' of https://github.com/izzzzzi/Solo_bot into izzzzzi-main 2024-11-11 23:35:49 +03:00
Zakhar Izmaylov 299a07e9a3 Fix texts 2024-11-11 23:35:01 +03:00
Zakhar Izmaylov abf5d686cb Small refactoring admin 2024-11-11 23:17:09 +03:00
Zakhar Izmaylov 00f4904b8c Fix bot.py 2024-11-11 23:11:14 +03:00
Zakhar Izmaylov c8b913b55a Fix, Update menu 2024-11-11 23:07:54 +03:00
Vladislav Lisitsyn 914d34c6f5 Merge pull request #83 from izzzzzi/main
Add multypay
2024-11-11 23:03:26 +03:00
Zakhar Izmaylov 6eb201198e Remove btn 2024-11-11 22:49:45 +03:00
Zakhar Izmaylov d088a6d161 Add multypay 2024-11-11 22:45:11 +03:00
Vladislav Lisitsyn 3a1268e97b Merge pull request #82 from izzzzzi/main
Add stars pay(beta)
2024-11-11 22:38:17 +03:00
Vladless 1c27ed7bf0 fix freekassa 2024-11-11 22:37:41 +03:00
Zakhar Izmaylov 2be5c74101 Merge branch 'main' of https://github.com/izzzzzi/Solo_bot 2024-11-11 22:33:03 +03:00
Zakhar Izmaylov ed34e26e80 Fix cryptobot 2024-11-11 22:31:52 +03:00
Zahar Izmailov 1cc5348270 Merge branch 'Vladless:main' into main 2024-11-11 22:28:00 +03:00
Zakhar Izmaylov f0f0469b1f Add stars pay(beta) 2024-11-11 22:26:10 +03:00
Vladislav Lisitsyn e9ff030b4b Merge pull request #81 from izzzzzi/main
Add crypto(beta), fix import, fix requirements.txt
2024-11-11 22:25:45 +03:00
Zakhar Izmaylov 84def9c3e3 Add crypto(beta), fix import, fix requirements.txt 2024-11-11 21:51:34 +03:00
Vladislav Lisitsyn 08daa6fcfc Update README.md 2024-11-11 17:51:44 +03:00
Vladislav Lisitsyn cd2d68d1b4 Update README.md 2024-11-11 17:51:12 +03:00
Vladislav Lisitsyn c480c03c31 Merge pull request #80 from izzzzzi/main
Add base cryptobot
2024-11-11 17:48:31 +03:00
Vladless 00efd1ce16 minPython 3.10/py3xui/subs/rm_artefacts 2024-11-11 17:47:26 +03:00
Zakhar Izmaylov 1869db7615 Add base cryptobot 2024-11-11 14:26:00 +03:00
Vladless a9e06b7a3b isort settings 2024-11-11 09:22:14 +03:00
Vladless 223559e44c bug fix 2024-11-11 07:42:22 +03:00
Vladislav Lisitsyn 627fd595da Merge pull request #79 from izzzzzi/main
Add make and config for formatting
2024-11-10 23:12:42 +03:00
Zakhar Izmaylov 56a4f8a51e Add make and config for formatting 2024-11-10 11:02:15 +03:00
Vladless c17dc371da reset traffic 2024-11-10 03:31:19 +03:00
Vladless 5e17847717 bug and fix 2024-11-10 02:25:36 +03:00
Vladislav Lisitsyn 0ec26c282b Merge pull request #78 from izzzzzi/main
Refactoring and fix
2024-11-09 23:59:29 +03:00
Zakhar Izmaylov 570ffcf818 Fix 2024-11-09 23:20:08 +03:00
Zakhar Izmaylov 5118256a1b Small fix 2024-11-09 16:23:39 +03:00
Zakhar Izmaylov d851692c0d Refactoring admin_panel.py 2024-11-09 15:54:16 +03:00
Zakhar Izmaylov a5a8560640 Refactoring notifications.py and commands.py 2024-11-09 15:49:05 +03:00
Zakhar Izmaylov 9518ef9f07 Refactoring profile.py 2024-11-09 15:35:22 +03:00
Zakhar Izmaylov f05cfb1c11 Refactoring start.py 2024-11-09 14:38:30 +03:00
Zakhar Izmaylov 0668335a5d Add middleware logging 2024-11-09 12:20:07 +03:00
Zakhar Izmaylov d40e6b4992 Add middlewaer datatabase # TODO 2024-11-09 12:17:07 +03:00
Zakhar Izmaylov 7d51ff3a85 Delete wraps 2024-11-09 12:01:24 +03:00
Zakhar Izmaylov eec5e8c887 isort and black 2024-11-09 11:44:15 +03:00
Zakhar Izmaylov b979748679 Migrate logging for loguru 2024-11-09 11:41:48 +03:00
Zakhar Izmaylov 71ded92b53 Fix logging 2024-11-09 11:33:17 +03:00
Zakhar Izmaylov 1ae030a256 Fix callback_query 2024-11-09 11:17:11 +03:00
Zakhar Izmaylov b0e3a8482c Fix PAYMENT_OPTIONS and RENEWAL_PLANS 2024-11-09 10:52:41 +03:00
Zakhar Izmaylov f44c6cb2e3 Refactoring and fix 2024-11-09 10:36:16 +03:00
Vladislav Lisitsyn a0d3eb3a52 Update README.md 2024-11-08 07:13:42 +03:00
Vladislav Lisitsyn e8aed23e1a Update README.md 2024-11-08 07:08:20 +03:00
Vladislav Lisitsyn 7300cc58c3 Merge pull request #77 from Vladless/subscriptions-feature
preview v2.
2024-11-08 07:07:00 +03:00
Vladless 045d362e93 preview v2. 2024-11-08 07:06:32 +03:00
Vladislav Lisitsyn eba46cd587 Merge pull request #76 from Vladless/subscriptions-feature
preview v3.
2024-11-08 07:05:30 +03:00
Vladless 31b87e7dd7 preview v3. 2024-11-08 07:04:51 +03:00
Vladislav Lisitsyn 7539a1cc93 Merge pull request #75 from Vladless/subscriptions-feature
v3. Поправлены уведомления для новой версии.
2024-11-08 06:53:12 +03:00
Vladless 109816199f v3. Поправлены уведомления для новой версии. 2024-11-08 06:52:37 +03:00
Vladislav Lisitsyn 1d56f85e6d Merge pull request #74 from Vladless/subscriptions-feature
v3. Админка переведена на новую версию.
2024-11-08 06:43:15 +03:00
Vladless ffcecb2d6c v3. Админка переведена на новую версию. 2024-11-08 06:42:12 +03:00
Vladislav Lisitsyn e598bf502f Merge pull request #73 from Vladless/subscriptions-feature
Админка
2024-11-08 06:03:34 +03:00
Vladless 918436f2cc v3. Админка переведена на новую версию. 2024-11-08 06:02:29 +03:00
Vladless 33e33b4be9 v3. Админка переведена на новую версию. 2024-11-08 06:02:07 +03:00
Vladislav Lisitsyn c342eb5d81 Update README.md 2024-11-07 07:58:58 +03:00
Vladislav Lisitsyn 3a178c5ab8 Update README.md 2024-11-07 07:58:41 +03:00
Vladislav Lisitsyn 166c4385a9 Merge pull request #72 from Vladless/subscriptions-feature
v3. Полноценные подписки
2024-11-07 07:56:55 +03:00
Vladless 9a67bf3976 v3. Полноценные подписки 2024-11-07 07:53:29 +03:00
53 changed files with 5435 additions and 2230 deletions
+6
View File
@@ -0,0 +1,6 @@
[flake8]
max-line-length = 250
ignore = E203, E266, E501, W503, F541, E704, W293, W291, E126, E121, E123, E128, E302, E131, E231, W292, E402, E261, E305
max-complexity = 25
select = B, C, E, F, W, T4, B9
exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,.txt
+52 -13
View File
@@ -1,14 +1,53 @@
/venv
/__pycache__
/vpn_users.db
/config.py
/database.db
/bot_old.py
/bot_old_2.py
/database.db
/backup_pg.sh
/config copy.py
/docker-compose.yml
__pycache__
/handlers/texts.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
dist/
build/
*.egg-info/
# Virtual environments
venv/
env/
.env/
.venv/
# IDE specific files
.idea/
.vscode/
*.sublime-project
*.sublime-workspace
# Logs and databases
*.log
*.sqlite3
*.db
# Sensitive configuration files
config.py
config.ini
.env
# Backup files
*.bak
*.swp
*~
# Specific project files
vpn_users.db
database.db
bot_old.py
bot_old_2.py
backup_pg.sh
docker-compose.yml
config copy.py
handlers/texts.py
# Miscellaneous
.DS_Store
Thumbs.db
nginx.conf
scripts/nginx.sh
+3
View File
@@ -0,0 +1,3 @@
[settings]
profile=black
line_length = 250
-3
View File
@@ -12,15 +12,12 @@ RUN apt-get update && \
sed -i '/ru_RU.UTF-8/s/^# //g' /etc/locale.gen && \
locale-gen ru_RU.UTF-8
# Устанавливаем переменные окружения
ENV LANG=ru_RU.UTF-8
ENV LANGUAGE=ru_RU:ru
ENV LC_ALL=ru_RU.UTF-8
COPY . .
# Замена DATABASE_URL на значение из переменной окружения
RUN sed -i "s|DATABASE_URL = .*|DATABASE_URL = '${DATABASE_URL}'|" config.py
# dosker-compose в полной версии бота
CMD ["python", "main.py"]
+2
View File
@@ -0,0 +1,2 @@
formatting:
@black . && isort . && flake8
+13 -5
View File
@@ -2,9 +2,16 @@
**SoloBot** — ваш идеальный помощник для управления API 3x-UI VPN на протоколе VLESS.
Две версии — море возможностей:
- v1.4 — бот для продажи ключей vless
- v2.0 — бот для продажи подписок vless (постоянно обновляется)
Три версии — море возможностей:
- v1.4 — бот для продажи ключей vless:
![Основные окна](preview.jpg)
- v2.3.1 — стабильная версия подписок вместо ключей с кнопками автодобавления в приложение:
![Основные окна](preview_v_2.jpg)
- v3.0.4_beta — полноценные подписки с выбором серверов из клиентского приложения (не в боте):
Если вам не хватает функций — направьте их в issue, мы реализуем
@@ -36,7 +43,8 @@ SoloBot реализует множество функций, включая:
- Доступ к **инструкциям**.
- **Пополнение баланса**:
* через Юкасса (самозанятость и ИП)
* через freekassa (Физические Лица)
* через freekassa (Физические Лица)
* Криптовалюта (Thanks [izzzzzi](https://github.com/izzzzzi))
- Периодические **бэкапы базы данных клиентов**.
- Уведомления:
- Произвольные сообщения через админку.
@@ -47,7 +55,7 @@ SoloBot реализует множество функций, включая:
- **Удобная админка прямо в боте**
- **мультисерверность** добавляй сервера в конфиг, и они автоматически будут в боте
![Основные окна](preview.jpg)
---
## 💻 Стек технологий
-70
View File
@@ -1,70 +0,0 @@
import json
import aiohttp
from config import SERVERS
session = None
async def login_with_credentials(server_id: str, username: str, password: str):
global session
session = aiohttp.ClientSession()
api_url = SERVERS[server_id]['API_URL']
auth_url = f"{api_url}/login/"
data = {
"username": username,
"password": password
}
async with session.post(auth_url, json=data) as response:
if response.status == 200:
session.cookie_jar.update_cookies(response.cookies)
return session
else:
raise Exception(f"Ошибка авторизации: {response.status}, {await response.text()}")
async def get_clients(session, server_id):
api_url = SERVERS[server_id]['API_URL']
async with session.get(f'{api_url}/panel/api/inbounds/list/') as response:
if response.status == 200:
return await response.json()
else:
raise Exception(f"Ошибка при получении клиентов: {response.status}, {await response.text()}")
async def link(session, server_id: str, client_id: str, email: str):
"""
Получение ссылки для подключения по ID клиента.
:param server_id: str - идентификатор сервера
:param client_id: str - идентификатор клиента
:param email: str - электронная почта клиента
:return: str - ссылка для подключения
"""
response = await get_clients(session, server_id)
if 'obj' not in response or len(response['obj']) == 0:
raise Exception("Не удалось получить данные клиентов.")
inbounds = response['obj'][0]
settings = json.loads(inbounds['settings'])
stream_settings = json.loads(inbounds['streamSettings'])
tcp = stream_settings.get('network', 'tcp')
reality = stream_settings.get('security', 'reality')
flow = stream_settings.get('flow', 'xtls-rprx-vision')
val = (
f"vless://{client_id}@{SERVERS[server_id]['DOMEN']}?type={tcp}&security={reality}&pbk={SERVERS[server_id]['PBK']}"
f"&fp=chrome&sni={SERVERS[server_id]['SNI']}&sid={SERVERS[server_id]['SID']}=%2F&flow={flow}#{SERVERS[server_id]['PREFIX']}-{email}"
)
return val
async def link_subscription(email, server_id):
server = SERVERS.get(server_id)
if server:
subscription_url = f"{server['SUBSCRIPTION']}/{email}"
return subscription_url
else:
raise ValueError(f"Server '{server_id}' not found in configuration.")
+32 -16
View File
@@ -1,10 +1,11 @@
import logging
import os
import subprocess
from datetime import datetime
from aiogram.types import BufferedInputFile
from config import ADMIN_ID, DB_NAME, DB_PASSWORD, DB_USER, BACK_DIR
from config import ADMIN_ID, BACK_DIR, DB_NAME, DB_PASSWORD, DB_USER
from logger import logger
async def backup_database():
@@ -16,33 +17,48 @@ async def backup_database():
DATE = datetime.now().strftime("%Y-%m-%d-%H%M%S")
BACKUP_FILE = f"{BACKUP_DIR}/{DB_NAME}-backup-{DATE}.sql"
os.environ['PGPASSWORD'] = DB_PASSWORD
os.environ["PGPASSWORD"] = DB_PASSWORD
try:
subprocess.run(
['pg_dump', '-U', USER, '-h', HOST, '-F', 'c', '-f', BACKUP_FILE, DB_NAME],
check=True
["pg_dump", "-U", USER, "-h", HOST, "-F", "c", "-f", BACKUP_FILE, DB_NAME],
check=True,
)
logging.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
logger.info(f"Бэкап базы данных создан: {BACKUP_FILE}")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при создании бэкапа базы данных: {e}")
logger.error(f"Ошибка при создании бэкапа базы данных: {e}")
return
try:
with open(BACKUP_FILE, 'rb') as backup_file:
backup_input_file = BufferedInputFile(backup_file.read(), filename=os.path.basename(BACKUP_FILE))
with open(BACKUP_FILE, "rb") as backup_file:
backup_input_file = BufferedInputFile(
backup_file.read(), filename=os.path.basename(BACKUP_FILE)
)
await bot.send_document(ADMIN_ID, backup_input_file)
logging.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
logger.info(f"Бэкап базы данных отправлен админу: {ADMIN_ID}")
except Exception as e:
logging.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
logger.error(f"Ошибка при отправке бэкапа в Telegram: {e}")
try:
subprocess.run(
['find', BACKUP_DIR, '-type', 'f', '-name', '*.sql', '-mtime', '+7', '-exec', 'rm', '{}', ';'],
check=True
[
"find",
BACKUP_DIR,
"-type",
"f",
"-name",
"*.sql",
"-mtime",
"+3",
"-exec",
"rm",
"{}",
";",
],
check=True,
)
logging.info("Старые бэкапы удалены.")
logger.info("Старые бэкапы удалены.")
except subprocess.CalledProcessError as e:
logging.error(f"Ошибка при удалении старых бэкапов: {e}")
logger.error(f"Ошибка при удалении старых бэкапов: {e}")
del os.environ['PGPASSWORD']
del os.environ["PGPASSWORD"]
+30 -7
View File
@@ -1,26 +1,49 @@
from aiogram import Bot, Dispatcher, Router
from aiogram.fsm.storage.memory import MemoryStorage
from config import API_TOKEN
from config import API_TOKEN, CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from middlewares.database import DatabaseMiddleware
from middlewares.logging import LoggingMiddleware
bot = Bot(token=API_TOKEN)
storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
router = Router()
from handlers.admin import admin, admin_panel, user_editor
from handlers import commands, coupons, donate, notifications, pay, profile, start
from handlers.admin import admin_commands, admin_coupons, admin_panel, admin_user_editor
from handlers.instructions import instructions
from handlers.keys import key_management, keys
from handlers.payment import pay, freekassa
from handlers import (notifications, profile, start, commands)
from handlers.payments import cryprobot_pay, freekassa_pay, robokassa_pay, stars_pay, yookassa_pay
dp.include_router(admin.router)
dp.include_router(admin_commands.router)
dp.include_router(admin_coupons.router)
dp.include_router(admin_panel.router)
dp.include_router(user_editor.router)
dp.include_router(admin_user_editor.router)
dp.include_router(commands.router)
dp.include_router(coupons.router)
dp.include_router(start.router)
dp.include_router(profile.router)
dp.include_router(keys.router)
dp.include_router(key_management.router)
dp.include_router(pay.router)
dp.include_router(freekassa.router)
dp.include_router(notifications.router)
dp.include_router(instructions.router)
dp.include_router(donate.router)
if YOOKASSA_ENABLE:
dp.include_router(yookassa_pay.router)
if FREEKASSA_ENABLE:
dp.include_router(freekassa_pay.router)
if CRYPTO_BOT_ENABLE:
dp.include_router(cryprobot_pay.router)
if STARS_ENABLE:
dp.include_router(stars_pay.router)
if ROBOKASSA_ENABLE:
dp.include_router(robokassa_pay.router)
dp.message.middleware(LoggingMiddleware())
dp.callback_query.middleware(LoggingMiddleware())
dp.message.middleware(DatabaseMiddleware())
dp.callback_query.middleware(DatabaseMiddleware())
+102 -167
View File
@@ -1,177 +1,112 @@
import json
import py3xui
from config import SERVERS
from config import TOTAL_GB
from logger import logger
async def add_client(session, server_id: str, client_id: str, email: str, tg_id: str, limit_ip: int, total_gb: int, expiry_time: int, enable: bool, flow: str):
api_url = SERVERS[server_id]['API_URL']
url = f'{api_url}/panel/api/inbounds/addClient'
email = email.lower()
client_data = {
"id": client_id,
"alterId": 0,
"email": email,
"limitIp": limit_ip,
"totalGB": total_gb,
"expiryTime": expiry_time,
"enable": enable,
"tgId": tg_id,
"subId": email,
"flow": flow,
}
settings = json.dumps({"clients": [client_data]})
data = {
"id": 1,
"settings": settings
}
headers = {
'Content-Type': 'application/json',
}
async with session.post(url, json=data, headers=headers) as response:
print(f"Запрос на добавление клиента: {data}")
print(f"Статус ответа: {response.status}")
response_text = await response.text()
print(f"Ответ от сервера: {response_text}")
if response.status == 200:
print(f"Клиент добавлен: email={email}")
return await response.json()
else:
print(f"Ошибка при добавлении клиента: {response.status}, {response_text}")
return None
async def extend_client_key(session, server_id: str, tg_id, client_id, email: str, new_expiry_time: int) -> bool:
api_url = SERVERS[server_id]['API_URL']
async with session.get(f"{api_url}/panel/api/inbounds/getClientTraffics/{email}") as response:
print(f"GET {response.url} Status: {response.status}")
response_text = await response.text()
print(f"GET Response: {response_text}")
if response.status != 200:
print(f"Ошибка при получении данных клиента: {response.status} - {response_text}")
return False
client_data = (await response.json()).get("obj", {})
print(client_data)
if not client_data:
print("Не удалось получить данные клиента.")
return False
current_expiry_time = client_data.get('expiryTime', 0)
if current_expiry_time == 0:
current_expiry_time = new_expiry_time
updated_expiry_time = max(current_expiry_time, new_expiry_time)
payload = {
"id": 1,
"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:
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'
}
async def add_client(
xui,
client_id: str,
email: str,
tg_id: str,
limit_ip: int,
total_gb: int,
expiry_time: int,
enable: bool,
flow: str,
):
"""
Adds a client to the server via 3x-ui.
"""
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
await xui.login()
client = py3xui.Client(
id=client_id,
email=email.lower(),
limit_ip=limit_ip,
total_gb=TOTAL_GB,
expiry_time=expiry_time,
enable=enable,
tg_id=tg_id,
sub_id=email,
flow=flow,
)
response = await xui.client.add(1, [client])
logger.info(f"Клиент {email} успешно добавлен с ID {client_id}.")
return response if response else {"status": "failed"}
except Exception as e:
print(f"Ошибка запроса: {e}")
return False
logger.error(f"Ошибка при добавлении клиента {email}: {e}")
return {"status": "failed", "error": str(e)}
async def delete_client(session, server_id: str, client_id: str) -> bool:
api_url = SERVERS[server_id]['API_URL']
url = f"{api_url}/panel/api/inbounds/1/delClient/{client_id}"
headers = {
'Accept': 'application/json'
}
async def extend_client_key(
xui, email: str, new_expiry_time: int, client_id: str, total_gb: int
):
"""
Функция для обновления срока действия ключа клиента по email.
"""
await xui.login()
try:
async with session.post(url, headers=headers) as response:
if response.status == 200:
return True
else:
print(f"Ошибка при удалении клиента: {response.status} - {await response.text()}")
return False
client = await xui.client.get_by_email(email)
if not client:
logger.warning(f"Клиент с email {email} не найден.")
return
if not client.id:
logger.warning(f"Ошибка: клиент {email} не имеет действительного ID.")
return
logger.info(
f"Обновление ключа клиента {client.email} с ID {client.id} до нового времени: {new_expiry_time}"
)
client.id = client_id
client.expiry_time = new_expiry_time
client.flow = "xtls-rprx-vision"
client.sub_id = email
client.total_gb = total_gb
client.enable = True
client.limit_ip = 1
await xui.client.update(client.id, client)
logger.info(
f"Ключ клиента {client.email} успешно продлён до {new_expiry_time}."
)
except Exception as e:
print(f"Ошибка запроса: {e}")
logger.error(f"Ошибка при обновлении клиента с email {email}: {e}")
async def delete_client(
xui,
email: str,
client_id: str,
) -> bool:
"""
Функция для удаления клиента с сервера 3x-ui.
Возвращает True при успешном удалении, иначе False.
"""
await xui.login()
try:
client = await xui.client.get_by_email(email)
if not client:
logger.warning(f"Клиент с email {email} и ID {client_id} не найден.")
return False
client.id = client_id
inbound_id = 1
await xui.client.delete(inbound_id, client.id)
logger.info(f"Клиент с ID {client_id} был удален успешно.")
return True
except Exception as e:
logger.error(f"Ошибка при удалении клиента с ID {client_id}: {e}")
return False
+260 -85
View File
@@ -2,208 +2,368 @@ from datetime import datetime
import asyncpg
from config import DATABASE_URL
from config import BONUS_PERCENT, DATABASE_URL
from logger import logger
async def init_db():
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
# Таблица для хранения информации о пользователях
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS connections (
tg_id BIGINT PRIMARY KEY NOT NULL,
balance REAL NOT NULL DEFAULT 0.0,
trial INTEGER NOT NULL DEFAULT 0
)
''')
await conn.execute('''
"""
)
# Таблица для хранения ключей
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS keys (
tg_id BIGINT NOT NULL,
client_id TEXT NOT NULL,
email TEXT NOT NULL,
created_at BIGINT NOT NULL,
expiry_time BIGINT NOT NULL,
expiry_time BIGINT NOT NULL,
key TEXT NOT NULL,
server_id TEXT NOT NULL DEFAULT 'server1', -- поле для идентификатора сервера
notified BOOLEAN NOT NULL DEFAULT FALSE, -- новое поле для статуса уведомления
server_id TEXT NOT NULL DEFAULT 'cluster1',
notified BOOLEAN NOT NULL DEFAULT FALSE,
notified_24h BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (tg_id, client_id)
)
''')
await conn.execute('''
"""
)
# Таблица для хранения рефералов
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS referrals (
referred_tg_id BIGINT PRIMARY KEY NOT NULL, -- ID приглашенного пользователя
referrer_tg_id BIGINT NOT NULL, -- ID пригласившего пользователя
reward_issued BOOLEAN DEFAULT FALSE -- Был ли начислен бонус
referred_tg_id BIGINT PRIMARY KEY NOT NULL,
referrer_tg_id BIGINT NOT NULL,
reward_issued BOOLEAN DEFAULT FALSE
)
''')
try:
await conn.execute('''
ALTER TABLE keys
ADD COLUMN server_id TEXT NOT NULL DEFAULT 'server1'
''')
except asyncpg.exceptions.DuplicateColumnError:
pass
try:
await conn.execute('''
ALTER TABLE keys
ADD COLUMN notified BOOLEAN NOT NULL DEFAULT FALSE
''')
except asyncpg.exceptions.DuplicateColumnError:
pass
try:
await conn.execute('''
ALTER TABLE keys
ADD COLUMN notified_24h BOOLEAN NOT NULL DEFAULT FALSE
''')
except asyncpg.exceptions.DuplicateColumnError:
pass
"""
)
# Таблица для хранения купонов
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS coupons (
id SERIAL PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
amount INTEGER NOT NULL,
usage_limit INTEGER NOT NULL DEFAULT 1,
usage_count INTEGER NOT NULL DEFAULT 0,
is_used BOOLEAN NOT NULL DEFAULT FALSE
)
"""
)
# Таблица для отслеживания использований купонов пользователями
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS coupon_usages (
coupon_id INTEGER NOT NULL REFERENCES coupons(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL,
used_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (coupon_id, user_id)
)
"""
)
await conn.close()
async def create_coupon(coupon_code: str, amount: float, usage_limit: int):
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute(
"""
INSERT INTO coupons (code, amount, usage_limit, usage_count, is_used)
VALUES ($1, $2, $3, 0, FALSE)
""",
coupon_code,
amount,
usage_limit,
)
except Exception as e:
print(f"Ошибка при создании купона: {e}")
raise
finally:
await conn.close()
async def get_all_coupons():
"""Получить список всех купонов"""
conn = await asyncpg.connect(DATABASE_URL)
try:
coupons = await conn.fetch(
"""
SELECT code, amount, usage_limit, usage_count
FROM coupons
"""
)
return coupons
except Exception as e:
print(f"Ошибка при получении купонов: {e}")
return []
finally:
await conn.close()
async def delete_coupon_from_db(coupon_code: str):
"""Удалить купон из базы данных по его коду"""
conn = await asyncpg.connect(DATABASE_URL)
try:
coupon_record = await conn.fetchrow(
"""
SELECT id FROM coupons WHERE code = $1
""",
coupon_code,
)
if not coupon_record:
return False
await conn.execute(
"""
DELETE FROM coupons WHERE code = $1
""",
coupon_code,
)
return True
except Exception as e:
print(f"Ошибка при удалении купона: {e}")
return False
finally:
await conn.close()
async def restore_trial(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute("UPDATE connections SET trial = 0 WHERE tg_id = $1", tg_id)
except Exception as e:
logger.error(f"Ошибка при установке значения триала: {e}")
finally:
await conn.close()
async def add_connection(tg_id: int, balance: float = 0.0, trial: int = 0):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
INSERT INTO connections (tg_id, balance, trial)
VALUES ($1, $2, $3)
''', tg_id, balance, trial)
""",
tg_id,
balance,
trial,
)
await conn.close()
async def check_connection_exists(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
exists = await conn.fetchval('''
exists = await conn.fetchval(
"""
SELECT EXISTS(SELECT 1 FROM connections WHERE tg_id = $1)
''', tg_id)
""",
tg_id,
)
await conn.close()
return exists
async def store_key(tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str):
async def store_key(
tg_id: int, client_id: str, email: str, expiry_time: int, key: str, server_id: str
):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
INSERT INTO keys (tg_id, client_id, email, created_at, expiry_time, key, server_id)
VALUES ($1, $2, $3, $4, $5, $6, $7)
''', tg_id, client_id, email, int(datetime.utcnow().timestamp() * 1000), expiry_time, key, server_id)
""",
tg_id,
client_id,
email,
int(datetime.utcnow().timestamp() * 1000),
expiry_time,
key,
server_id,
)
await conn.close()
async def get_keys(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
records = await conn.fetch('''
records = await conn.fetch(
"""
SELECT client_id, email, created_at, key
FROM keys
WHERE tg_id = $1
''', tg_id)
""",
tg_id,
)
await conn.close()
return records
async def get_keys_by_server(tg_id: int, server_id: str):
conn = await asyncpg.connect(DATABASE_URL)
records = await conn.fetch('''
records = await conn.fetch(
"""
SELECT client_id, email, created_at, key
FROM keys
WHERE tg_id = $1 AND server_id = $2
''', tg_id, server_id)
""",
tg_id,
server_id,
)
await conn.close()
return records
async def has_active_key(tg_id: int) -> bool:
conn = await asyncpg.connect(DATABASE_URL)
count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id)
await conn.close()
return count > 0
async def get_balance(tg_id: int) -> float:
conn = await asyncpg.connect(DATABASE_URL)
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
balance = await conn.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
await conn.close()
return balance if balance is not None else 0.0
async def update_balance(tg_id: int, amount: float):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
UPDATE connections
SET balance = balance + $1
await conn.execute(
"""
UPDATE connections
SET balance = balance + $1
WHERE tg_id = $2
''', amount, tg_id)
""",
amount,
tg_id,
)
await handle_referral_on_balance_update(tg_id, amount)
await conn.close()
async def get_trial(tg_id: int) -> int:
conn = await asyncpg.connect(DATABASE_URL)
trial = await conn.fetchval("SELECT trial FROM connections WHERE tg_id = $1", tg_id)
await conn.close()
return trial if trial is not None else 0
async def get_key_count(tg_id: int) -> int:
conn = await asyncpg.connect(DATABASE_URL)
count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE tg_id = $1', tg_id)
count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE tg_id = $1", tg_id)
await conn.close()
return count if count is not None else 0
async def get_all_users(conn):
return await conn.fetch('SELECT tg_id FROM connections')
return await conn.fetch("SELECT tg_id FROM connections")
async def add_referral(referred_tg_id: int, referrer_tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
INSERT INTO referrals (referred_tg_id, referrer_tg_id)
VALUES ($1, $2)
''', referred_tg_id, referrer_tg_id)
""",
referred_tg_id,
referrer_tg_id,
)
await conn.close()
async def handle_referral_on_balance_update(tg_id: int, amount: float):
conn = await asyncpg.connect(DATABASE_URL)
referral = await conn.fetchrow('''
referral = await conn.fetchrow(
"""
SELECT referrer_tg_id FROM referrals WHERE referred_tg_id = $1
''', tg_id)
""",
tg_id,
)
if referral:
referrer_tg_id = referral['referrer_tg_id']
bonus = amount * 0.25
referrer_tg_id = referral["referrer_tg_id"]
bonus = amount * BONUS_PERCENT
if bonus < 0:
bonus = 0
await update_balance(referrer_tg_id, bonus)
await conn.execute('''
UPDATE referrals SET reward_issued = TRUE
await conn.execute(
"""
UPDATE referrals SET reward_issued = TRUE
WHERE referrer_tg_id = $1 AND referred_tg_id = $2
''', referrer_tg_id, tg_id)
""",
referrer_tg_id,
tg_id,
)
await conn.close()
async def get_referral_stats(referrer_tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
total_referrals = await conn.fetchval('''
total_referrals = await conn.fetchval(
"""
SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1
''', referrer_tg_id)
""",
referrer_tg_id,
)
active_referrals = await conn.fetchval('''
active_referrals = await conn.fetchval(
"""
SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1 AND reward_issued = TRUE
''', referrer_tg_id)
""",
referrer_tg_id,
)
await conn.close()
return {
'total_referrals': total_referrals,
'active_referrals': active_referrals
}
return {"total_referrals": total_referrals, "active_referrals": active_referrals}
async def update_key_expiry(client_id: str, new_expiry_time: int):
"""
Обновление времени истечения ключа на новое значение.
"""
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
UPDATE keys
SET expiry_time = $1, notified = FALSE, notified_24h = FALSE
WHERE client_id = $2
''', new_expiry_time, client_id)
""",
new_expiry_time,
client_id,
)
await conn.close()
@@ -212,36 +372,51 @@ async def delete_key(client_id: str):
Удаление ключа из базы данных.
"""
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
DELETE FROM keys
WHERE client_id = $1
''', client_id)
""",
client_id,
)
await conn.close()
async def add_balance_to_client(client_id: str, amount: float):
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute('''
await conn.execute(
"""
UPDATE connections
SET balance = balance + $1
WHERE tg_id = $2
''', amount, client_id)
""",
amount,
client_id,
)
await conn.close()
async def get_client_id_by_email(email: str):
"""
Получение client_id по email.
"""
conn = await asyncpg.connect(DATABASE_URL)
client_id = await conn.fetchval('''
client_id = await conn.fetchval(
"""
SELECT client_id FROM keys WHERE email = $1
''', email)
""",
email,
)
await conn.close()
return client_id
async def get_tg_id_by_client_id(client_id: str):
conn = await asyncpg.connect(DATABASE_URL)
try:
result = await conn.fetchrow('SELECT tg_id FROM keys WHERE client_id = $1', client_id)
return result['tg_id'] if result else None
result = await conn.fetchrow(
"SELECT tg_id FROM keys WHERE client_id = $1", client_id
)
return result["tg_id"] if result else None
finally:
await conn.close()
await conn.close()
+14
View File
@@ -0,0 +1,14 @@
from aiogram.filters import BaseFilter
from aiogram.types import Message
from config import ADMIN_ID
class IsAdminFilter(BaseFilter):
async def __call__(self, message: Message) -> bool:
if isinstance(ADMIN_ID, list):
return message.from_user.id in ADMIN_ID
elif isinstance(ADMIN_ID, int):
return message.from_user.id == ADMIN_ID
else:
return False
-82
View File
@@ -1,82 +0,0 @@
from aiogram import Router, types
from aiogram.filters import Command
from database import add_balance_to_client, check_connection_exists, update_key_expiry, get_client_id_by_email, get_tg_id_by_client_id
from config import ADMIN_ID, DATABASE_URL, ADMIN_PASSWORD, ADMIN_USERNAME
from datetime import datetime
import asyncpg
from auth import login_with_credentials
from client import extend_client_key_admin
router = Router()
@router.message(Command('add_balance'))
async def cmd_add_balance(message: types.Message):
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
try:
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"Клиент с ID {client_id} не найден.")
return
await add_balance_to_client(int(client_id), amount)
await message.reply(f"Баланс клиента {client_id} увеличен на {amount} у.е.")
except ValueError:
await message.reply("Пожалуйста, используйте формат: /add_balance <client_id> <amount>")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
@router.message(Command('update_key_expiry'))
async def cmd_update_key_expiry(message: types.Message):
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
try:
parts = message.text.split(maxsplit=2)
if len(parts) != 3:
await message.reply("Пожалуйста, используйте формат: /update_key_expiry <email> <expiry_time(YYYY-MM-DD HH:MM:SS)>")
return
_, email, expiry_time_str = parts
expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
return
await update_key_expiry(client_id, expiry_time)
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id)
if not record:
await message.reply("Клиент не найден в базе данных.")
return
server_id = record['server_id']
tg_id = await get_tg_id_by_client_id(client_id)
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
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)
print(f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}")
if success:
await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено и синхронизировано с панелью.")
else:
await message.reply(f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью.")
finally:
await conn.close()
except ValueError:
await message.reply("Пожалуйста, используйте формат: /update_key_expiry <email> <expiry_time(YYYY-MM-DD HH:MM:SS)>")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
+160
View File
@@ -0,0 +1,160 @@
import asyncpg
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from bot import bot
from config import DATABASE_URL
from database import add_balance_to_client, check_connection_exists
from filters.admin import IsAdminFilter
from handlers.texts import TRIAL
from logger import logger
router = Router()
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
@router.message(Command("add_balance"), IsAdminFilter())
async def cmd_add_balance(message: types.Message):
try:
_, client_id, amount = message.text.split()
amount = float(amount)
if not await check_connection_exists(int(client_id)):
await message.reply(f"❌ Клиент с ID {client_id} не найден в базе данных.")
return
await add_balance_to_client(int(client_id), amount)
await message.reply(
f"✅ Баланс клиента {client_id} успешно пополнен на {amount}"
)
except ValueError:
await message.reply(
"❓ Неверный формат команды!\n"
"Пожалуйста, используйте следующий шаблон:\n"
"/add_balance <ID клиента> <сумма пополнения>"
)
except Exception as e:
await message.reply(f"🚨 Произошла непредвиденная ошибка: {e}")
@router.message(Command("backup"), IsAdminFilter())
async def backup_command(message: types.Message):
from backup import backup_database
await message.answer("🔄 Инициализация резервного копирования базы данных...")
await backup_database()
await message.answer(
"✅ Бэкап базы данных успешно завершен и отправлен администратору."
)
@router.message(Command("send_trial"), IsAdminFilter())
async def handle_send_trial_command(message: types.Message, state: FSMContext):
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch(
"""
SELECT tg_id FROM connections WHERE trial = 0
"""
)
if records:
success_count = 0
error_count = 0
blocked_count = 0
for record in records:
tg_id = record["tg_id"]
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
success_count += 1
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
blocked_count += 1
logger.info(
f"🚫 Бот заблокирован пользователем с tg_id: {tg_id}"
)
else:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
f"📊 Результаты рассылки пробных периодов:\n"
f"✅ Успешно отправлено: {success_count}\n"
f"🚫 Заблокировано: {blocked_count}\n"
f"❌ Ошибок: {error_count}"
)
else:
await message.answer(
"📭 Нет пользователей с неиспользованными пробными ключами."
)
finally:
await conn.close()
except Exception as e:
await message.answer(f"❗ Ошибка при отправке сообщений: {e}")
@router.message(Command("send_to_all"), IsAdminFilter())
async def send_message_to_all_clients(
message: types.Message, state: FSMContext, from_panel=False
):
if from_panel:
await message.answer(
"✍️ Введите текст сообщения, который вы хотите отправить всем клиентам:"
)
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message, IsAdminFilter())
async def process_message_to_all(
message: types.Message,
state: FSMContext,
):
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch("SELECT tg_id FROM connections")
total_users = len(tg_ids)
success_count = 0
error_count = 0
for record in tg_ids:
tg_id = record["tg_id"]
try:
await bot.send_message(chat_id=tg_id, text=text_message)
success_count += 1
except Exception as e:
error_count += 1
logger.error(
f"❌ Ошибка при отправке сообщения пользователю {tg_id}: {e}"
)
await message.answer(
f"📤 Рассылка завершена:\n"
f"👥 Всего пользователей: {total_users}\n"
f"✅ Успешно отправлено: {success_count}\n"
f"❌ Не доставлено: {error_count}"
)
except Exception as e:
logger.error(f"❗ Ошибка при подключении к базе данных: {e}")
await message.answer("❌ Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await state.clear()
+207
View File
@@ -0,0 +1,207 @@
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from database import create_coupon, delete_coupon_from_db, get_all_coupons
from logger import logger
class AdminCouponsState(StatesGroup):
waiting_for_coupon_data = State()
router = Router()
@router.callback_query(F.data == "coupons_editor")
async def show_coupon_management_menu(callback_query: types.CallbackQuery):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text=" Создать купон", callback_data="create_coupon")
)
builder.row(InlineKeyboardButton(text="Купоны", callback_data="coupons"))
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_admin_menu")
)
markup = builder.as_markup()
await callback_query.message.answer(
"🛠 Меню управления купонами:", reply_markup=markup
)
await callback_query.answer()
@router.callback_query(F.data == "coupons")
async def show_coupon_list(callback_query: types.CallbackQuery):
try:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
coupons = await get_all_coupons()
if not coupons:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
markup = builder.as_markup()
await callback_query.message.answer(
"❌ На данный момент нет доступных купонов.\n"
"Вы можете вернуться в меню управления.",
parse_mode="HTML",
reply_markup=markup,
)
await callback_query.answer()
return
coupon_list = "📜 Список всех купонов:\n\n"
builder = InlineKeyboardBuilder()
for coupon in coupons:
coupon_list += (
f"<b>Код:</b> {coupon['code']}\n"
f"<b>Сумма:</b> {coupon['amount']} рублей\n"
f"<b>Лимит использования:</b> {coupon['usage_limit']} раз\n"
f"<b>Использовано:</b> {coupon['usage_count']} раз\n\n"
)
builder.row(
InlineKeyboardButton(
text=f"❌ Удалить {coupon['code']}",
callback_data=f"delete_coupon_{coupon['code']}",
)
)
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
markup = builder.as_markup()
await callback_query.message.answer(
coupon_list, parse_mode="HTML", reply_markup=markup
)
except Exception as e:
logger.error(f"Ошибка при получении списка купонов: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при получении списка купонов: {e}", parse_mode="HTML"
)
await callback_query.answer()
@router.callback_query(F.data.startswith("delete_coupon_"))
async def handle_delete_coupon(callback_query: types.CallbackQuery):
coupon_code = callback_query.data[len("delete_coupon_") :]
try:
result = await delete_coupon_from_db(coupon_code)
if result:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await show_coupon_list(callback_query)
else:
await callback_query.message.answer(
f"❌ Купон с кодом <b>{coupon_code}</b> не найден.", parse_mode="HTML"
)
await show_coupon_list(callback_query)
except Exception as e:
logger.error(f"Ошибка при удалении купона: {e}")
await callback_query.message.answer(
f"❌ Произошла ошибка при удалении купона: {e}", parse_mode="HTML"
)
await callback_query.answer()
@router.callback_query(F.data == "create_coupon")
async def handle_create_coupon(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer(
"<b>Введите данные для создания купона в формате:</b>\n\n"
"<i>код</i> <i>сумма</i> <i>лимит</i>\n\n"
"Пример: <b>'COUPON1 50 5'</b>\n\n",
parse_mode="HTML",
)
await state.set_state(AdminCouponsState.waiting_for_coupon_data)
await callback_query.answer()
@router.message(AdminCouponsState.waiting_for_coupon_data)
async def process_coupon_data(message: types.Message, state: FSMContext):
text = message.text.strip()
parts = text.split()
if len(parts) != 3:
await message.answer(
"<b>❌ Некорректный формат!</b> Пожалуйста, введите данные в формате:\n"
"<b>код</b> <b>сумма</b> <b>лимит</b>\n"
"Пример: <b>'COUPON1 50 5'</b>",
parse_mode="HTML",
)
return
try:
coupon_code = parts[0]
coupon_amount = float(parts[1])
usage_limit = int(parts[2])
except ValueError:
await message.answer(
"<b>⚠️ Проверьте правильность введенных данных.</b>\n"
"Сумма должна быть числом, а лимит — целым числом.",
parse_mode="HTML",
)
return
try:
await create_coupon(coupon_code, coupon_amount, usage_limit)
result_message = (
f"✅ Купон с кодом <b>{coupon_code}</b> успешно создан!\n"
f"Сумма: <b>{coupon_amount} рублей</b>\n"
f"Лимит использования: <b>{usage_limit} раз</b>."
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="coupons_editor")
)
markup = builder.as_markup()
try:
await message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await message.answer(result_message, parse_mode="HTML", reply_markup=markup)
await state.clear()
except Exception as e:
logger.error(f"Ошибка при создании купона: {e}")
await message.answer(
f"<b>❌ Ошибка при создании купона:</b> {e}", parse_mode="HTML"
)
@router.callback_query(F.data == "back_to_coupons_menu")
async def back_to_coupons_menu(callback_query: types.CallbackQuery):
"""Возвращаем пользователя в меню управления купонами"""
await show_coupon_management_menu(callback_query)
+149 -65
View File
@@ -1,39 +1,61 @@
from aiogram import Router, types
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery
from aiogram.filters import Command
from aiogram.fsm.state import StatesGroup, State
from config import ADMIN_ID, DATABASE_URL
import asyncpg
from datetime import datetime
from bot import bot
import subprocess
from backup import backup_database
from handlers.commands import send_message_to_all_clients
from aiogram.types import Message
from datetime import datetime
import asyncpg
from aiogram import F, Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from backup import backup_database
from bot import bot
from config import DATABASE_URL
from filters.admin import IsAdminFilter
from handlers.admin.admin_commands import send_message_to_all_clients
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
@router.message(Command('admin'))
@router.message(Command("admin"), IsAdminFilter())
async def handle_admin_command(message: types.Message):
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📊 Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="👥 Управление пользователями", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Управление купонами", callback_data="coupons_editor"
)
)
builder.row(
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")
)
builder.row(
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
)
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
await bot.send_message(
message.chat.id, "🤖 Панель администратора", reply_markup=builder.as_markup()
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")],
[InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")],
[InlineKeyboardButton(text="Отправить сообщение всем клиентам", callback_data="send_to_alls")],
[InlineKeyboardButton(text="Создать бэкап", callback_data="backups")],
[InlineKeyboardButton(text="Перезапустить бота", callback_data="restart_bot")]
])
await message.reply("Панель администратора", reply_markup=keyboard)
@router.callback_query(lambda c: c.data == "user_stats")
@router.callback_query(F.data == "user_stats", IsAdminFilter())
async def user_stats_menu(callback_query: CallbackQuery):
conn = await asyncpg.connect(DATABASE_URL)
try:
@@ -41,67 +63,129 @@ async def user_stats_menu(callback_query: CallbackQuery):
total_keys = await conn.fetchval("SELECT COUNT(*) FROM keys")
total_referrals = await conn.fetchval("SELECT COUNT(*) FROM referrals")
active_keys = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE expiry_time > $1", int(datetime.utcnow().timestamp() * 1000))
active_keys = await conn.fetchval(
"SELECT COUNT(*) FROM keys WHERE expiry_time > $1",
int(datetime.utcnow().timestamp() * 1000),
)
expired_keys = total_keys - active_keys
stats_message = (
f"🔹 <b>Общая статистика пользователей:</b>\n"
f"• Всего пользователей: <b>{total_users}</b>\n"
f" Всего ключей: <b>{total_keys}</b>\n"
f"• Всего рефералов: <b>{total_referrals}</b>\n"
f"• Активные ключи: <b>{active_keys}</b>\n"
f"• Истекшие ключи: <b>{expired_keys}</b>"
f"📈 <b>Подробная статистика проекта:</b>\n\n"
f"👤 Зарегистрированных пользователей: <b>{total_users}</b>\n"
f"🔑 Всего сгенерированных ключей: <b>{total_keys}</b>\n"
f"🤝 Привлеченных рефералов: <b>{total_referrals}</b>\n"
f"✅ Действующих ключей: <b>{active_keys}</b>\n"
f"❌ Просроченных ключей: <b>{expired_keys}</b>"
)
back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[back_button]
])
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Вернуться в меню", callback_data="back_to_admin_menu"
)
)
await callback_query.message.edit_text(stats_message, reply_markup=keyboard, parse_mode="HTML")
await callback_query.message.edit_text(
stats_message, reply_markup=builder.as_markup(), parse_mode="HTML"
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(lambda c: c.data == "send_to_alls")
@router.callback_query(F.data == "send_to_alls", IsAdminFilter())
async def handle_send_to_all(callback_query: CallbackQuery, state: FSMContext):
await send_message_to_all_clients(callback_query.message, state, from_panel=True)
await callback_query.answer()
await callback_query.answer()
@router.callback_query(lambda c: c.data == "backups")
@router.callback_query(F.data == "backups", IsAdminFilter())
async def handle_backup(message: Message):
await message.answer("Запускаю бэкап базы данных...")
await message.answer("💾 Инициализация резервного копирования базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
await message.answer(
"✅ Резервная копия успешно создана и отправлена администратору."
)
@router.callback_query(lambda c: c.data == "restart_bot")
@router.callback_query(F.data == "restart_bot", IsAdminFilter())
async def handle_restart(callback_query: CallbackQuery):
if callback_query.from_user.id == ADMIN_ID:
try:
result = subprocess.run(['sudo', 'systemctl', 'restart', 'bot.service'], check=True, capture_output=True, text=True)
await callback_query.message.answer("Бот успешно перезапущен.")
except subprocess.CalledProcessError as e:
await callback_query.message.answer(f"Бот будет перезапущен через 30 секунд {e.stderr}")
else:
await callback_query.answer("У вас нет доступа к этой команде.", show_alert=True)
try:
subprocess.run(
["sudo", "systemctl", "restart", "bot.service"],
check=True,
capture_output=True,
text=True,
)
await callback_query.message.answer("🔄 Бот успешно перезапущен.")
except subprocess.CalledProcessError as e:
await callback_query.message.answer(
f"⚠️ Перезагрузка бота будет выполнена через 30 секунд. Детали: {e.stderr}"
)
@router.callback_query(lambda c: c.data == "user_editor")
@router.callback_query(F.data == "user_editor", IsAdminFilter())
async def user_editor_menu(callback_query: CallbackQuery):
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Поиск по имени ключа", callback_data="search_by_key_name")],
[InlineKeyboardButton(text="Поиск по tg_id", callback_data="search_by_tg_id")],
[InlineKeyboardButton(text="Назад", callback_data="back_to_admin_menu")] # Back button
])
await callback_query.message.edit_text("Выберите метод поиска:", reply_markup=keyboard)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔍 Поиск по названию ключа", callback_data="search_by_key_name"
)
)
builder.row(
InlineKeyboardButton(
text="🆔 Поиск по Telegram ID", callback_data="search_by_tg_id"
)
)
builder.row(
InlineKeyboardButton(
text="🔙 Вернуться назад", callback_data="back_to_admin_menu"
)
)
await callback_query.message.edit_text(
"👇 Выберите способ поиска пользователя:", reply_markup=builder.as_markup()
)
@router.callback_query(lambda c: c.data == "back_to_admin_menu")
@router.callback_query(F.data == "back_to_admin_menu", IsAdminFilter())
async def back_to_admin_menu(callback_query: CallbackQuery):
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Статистика пользователей", callback_data="user_stats")],
[InlineKeyboardButton(text="Редактор пользователей", callback_data="user_editor")]
])
await callback_query.message.edit_text("Панель администратора", reply_markup=keyboard)
try:
await callback_query.message.delete()
except Exception:
pass
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="📊 Статистика пользователей", callback_data="user_stats"
)
)
builder.row(
InlineKeyboardButton(
text="👥 Управление пользователями", callback_data="user_editor"
)
)
builder.row(
InlineKeyboardButton(text="📢 Массовая рассылка", callback_data="send_to_alls")
)
builder.row(
InlineKeyboardButton(
text="🎟️ Управление купонами", callback_data="coupons_editor"
)
)
builder.row(
InlineKeyboardButton(text="💾 Создать резервную копию", callback_data="backups")
)
builder.row(
InlineKeyboardButton(text="🔄 Перезагрузить бота", callback_data="restart_bot")
)
await bot.send_message(
tg_id, "🤖 Панель администратора", reply_markup=builder.as_markup()
)
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.edit_message_text(
message, chat_id=tg_id, message_id=callback_query.message.message_id
)
+543
View File
@@ -0,0 +1,543 @@
import asyncio
from datetime import datetime
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CLUSTERS, DATABASE_URL, TOTAL_GB
from database import get_client_id_by_email, restore_trial, update_key_expiry
from filters.admin import IsAdminFilter
from handlers.admin.admin_panel import back_to_admin_menu
from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster
from handlers.utils import sanitize_key_name
from logger import logger
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
waiting_for_new_balance = State()
waiting_for_key_name = State()
waiting_for_expiry_time = State()
@router.callback_query(F.data == "search_by_tg_id", IsAdminFilter())
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("🔍 Введите Telegram ID клиента:")
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit(), IsAdminFilter())
async def handle_tg_id_input(message: types.Message, state: FSMContext):
tg_id = int(message.text)
conn = await asyncpg.connect(DATABASE_URL)
try:
balance = await conn.fetchval(
"SELECT balance FROM connections WHERE tg_id = $1", tg_id
)
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
)
if balance is None:
await message.reply("Пользователь с указанным tg_id не найден.")
await state.clear()
return
builder = InlineKeyboardBuilder()
for (email,) in key_records:
builder.row(
InlineKeyboardButton(
text=f"🔑 {email}", callback_data=f"edit_key_{email}"
)
)
builder.row(
InlineKeyboardButton(
text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(
text="🔄 Восстановить пробник", callback_data=f"restore_trial_{tg_id}"
)
)
builder.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_user_editor")
)
user_info = (
f"📊 Информация о пользователе:\n"
f"💰 Баланс: <b>{balance}</b>\n"
f"👥 Количество рефералов: <b>{referral_count}</b>\n"
f"🔑 Ключи (для редактирования нажмите на ключ):"
)
await message.reply(
user_info, reply_markup=builder.as_markup(), parse_mode="HTML"
)
await state.set_state(UserEditorState.displaying_user_info)
finally:
await conn.close()
@router.callback_query(F.data.startswith("restore_trial_"), IsAdminFilter())
async def handle_restore_trial(callback_query: types.CallbackQuery):
tg_id = int(callback_query.data.split("_")[2])
await restore_trial(tg_id)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора", callback_data="back_to_user_editor"
)
)
await callback_query.message.edit_text(
"✅ Триал успешно восстановлен.", reply_markup=builder.as_markup()
)
@router.callback_query(F.data.startswith("change_balance_"), IsAdminFilter())
async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
tg_id = int(callback_query.data.split("_")[2])
await state.update_data(tg_id=tg_id)
await callback_query.message.edit_text("💸 Введите новую сумму баланса:")
await callback_query.answer()
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance, IsAdminFilter())
async def handle_new_balance_input(message: types.Message, state: FSMContext):
if not message.text.isdigit() or int(message.text) < 0:
await message.reply(
"❌ Пожалуйста, введите корректную сумму для изменения баланса."
)
return
new_balance = int(message.text)
user_data = await state.get_data()
tg_id = user_data.get("tg_id")
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute(
"UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id
)
response_message = f"✅ Баланс успешно изменен на <b>{new_balance}</b>."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора",
callback_data="back_to_user_editor",
)
)
await message.reply(
response_message, reply_markup=builder.as_markup(), parse_mode="HTML"
)
finally:
await conn.close()
await state.clear()
@router.callback_query(F.data.startswith("edit_key_"), IsAdminFilter())
async def process_key_edit(callback_query: CallbackQuery):
email = callback_query.data.split("_", 2)[2]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.key, k.expiry_time, k.server_id
FROM keys k
WHERE k.email = $1
""",
email,
)
if record:
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = "Неизвестный сервер"
for cluster in CLUSTERS.values():
if server_id in cluster:
server_name = cluster[server_id].get(
"name", "Неизвестный сервер"
)
break
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = (
f"Ключ: <pre>{key}</pre>\n"
f"Дата истечения: <b>{formatted_expiry_date}</b>\n"
f"{days_left_message}\n"
f"Сервер: <b>{server_name}</b>"
)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
),
InlineKeyboardButton(
text="❌ Удалить ключ",
callback_data=f"delete_key_admin|{email}",
),
)
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data="back_to_user_editor"
)
)
await callback_query.message.edit_text(
response_message,
reply_markup=builder.as_markup(),
parse_mode="HTML",
)
else:
await callback_query.message.edit_text(
"<b>Информация о ключе не найдена.</b>", parse_mode="HTML"
)
finally:
await conn.close()
except Exception as e:
logger.error(f"Ошибка при получении информации о ключе: {e}")
await callback_query.answer()
@router.callback_query(F.data == "search_by_key_name", IsAdminFilter())
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("🔑 Введите имя ключа:")
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name, IsAdminFilter())
async def handle_key_name_input(message: types.Message, state: FSMContext):
key_name = sanitize_key_name(message.text)
conn = await asyncpg.connect(DATABASE_URL)
try:
user_records = await conn.fetch(
"""
SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.email = $1
""",
key_name,
)
if not user_records:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад в меню администратора",
callback_data="back_to_user_editor",
)
)
await message.reply(
"🚫 Пользователь с указанным именем ключа не найден.",
reply_markup=builder.as_markup(),
)
await state.clear()
return
response_messages = []
key_buttons = InlineKeyboardBuilder()
for record in user_records:
balance = record["balance"]
email = record["email"]
key = record["key"]
expiry_time = record["expiry_time"]
server_id = record["server_id"]
server_name = "Неизвестный сервер"
for cluster in CLUSTERS.values():
if server_id in cluster:
server_name = cluster[server_id].get("name", "Неизвестный сервер")
break
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime(
"%d %B %Y"
)
response_messages.append(
f"🔑 Ключ: <pre>{key}</pre>\n"
f"⏰ Дата истечения: <b>{expiry_date}</b>\n"
f"💰 Баланс пользователя: <b>{balance}</b>\n"
f"🌐 Сервер: <b>{server_name}</b>"
)
key_buttons.row(
InlineKeyboardButton(
text="⏳ Изменить время истечения",
callback_data=f"change_expiry|{email}",
)
)
key_buttons.row(
InlineKeyboardButton(
text="❌ Удалить ключ", callback_data=f"delete_key_admin|{email}"
)
)
key_buttons.row(
InlineKeyboardButton(text="🔙 Назад", callback_data="back_to_user_editor")
)
await message.reply(
"\n".join(response_messages),
reply_markup=key_buttons.as_markup(),
parse_mode="HTML",
)
finally:
await conn.close()
await state.clear()
@router.callback_query(F.data.startswith("change_expiry|"), IsAdminFilter())
async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
email = callback_query.data.split("|")[1]
await callback_query.message.edit_text(
f"⏳ Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:",
parse_mode="HTML",
)
await state.update_data(email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(UserEditorState.waiting_for_expiry_time, IsAdminFilter())
async def handle_expiry_time_input(message: types.Message, state: FSMContext):
user_data = await state.get_data()
email = user_data.get("email")
if not email:
await message.reply("Email не найден в состоянии.")
await state.clear()
return
try:
expiry_time_str = message.text
expiry_time = int(
datetime.strptime(expiry_time_str, "%Y-%m-%d %H:%M:%S").timestamp() * 1000
)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
await state.clear()
return
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"SELECT server_id FROM keys WHERE client_id = $1", client_id
)
if not record:
await message.reply("Клиент не найден в базе данных.")
await state.clear()
return
async def update_key_on_all_servers():
tasks = []
for cluster_id in CLUSTERS:
tasks.append(
asyncio.create_task(
renew_key_in_cluster(
cluster_id,
email,
client_id,
expiry_time,
total_gb=TOTAL_GB,
)
)
)
await asyncio.gather(*tasks)
await update_key_on_all_servers()
await update_key_expiry(client_id, expiry_time)
response_message = f"✅ Время истечения ключа для клиента {client_id} ({email}) успешно обновлено на всех серверах."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🔙 Назад", callback_data="back_to_user_editor"
)
)
await message.reply(
response_message, reply_markup=builder.as_markup(), parse_mode="HTML"
)
finally:
await conn.close()
except ValueError:
await message.reply("❌ Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
await state.clear()
@router.callback_query(F.data.startswith("delete_key_admin|"), IsAdminFilter())
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
email = callback_query.data.split("|")[1]
conn = await asyncpg.connect(DATABASE_URL)
try:
client_id = await conn.fetchval(
"SELECT client_id FROM keys WHERE email = $1", email
)
if client_id is None:
await bot.edit_message_text(
"Ключ не найден.",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
return
builder = InlineKeyboardBuilder()
builder.row(
types.InlineKeyboardButton(
text="✅ Да, удалить", callback_data=f"confirm_delete_admin|{client_id}"
)
)
builder.row(
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=builder.as_markup(),
parse_mode="HTML",
)
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(F.data.startswith("confirm_delete_admin|"), IsAdminFilter())
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"SELECT email FROM keys WHERE client_id = $1", client_id
)
if record:
email = record["email"]
response_message = "✅ Ключ успешно удален."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")
)
async def delete_key_from_servers(email, client_id):
tasks = []
for cluster_id in CLUSTERS:
tasks.append(
delete_key_from_cluster(cluster_id, email, client_id)
)
await asyncio.gather(*tasks)
await delete_key_from_servers(email, 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=builder.as_markup(),
)
else:
response_message = "🚫 Ключ не найден или уже удален."
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="view_keys")
)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=builder.as_markup(),
)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(
f"Ошибка при удалении ключа: {e}",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
await callback_query.answer()
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
@router.callback_query(F.data == "back_to_user_editor")
async def back_to_user_editor(callback_query: CallbackQuery):
await back_to_admin_menu(callback_query)
-368
View File
@@ -1,368 +0,0 @@
from aiogram import Router, types, F
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import StatesGroup, State
from config import DATABASE_URL, SERVERS
import asyncpg
from datetime import datetime
from bot import bot
from database import update_key_expiry, get_client_id_by_email, get_tg_id_by_client_id
from config import DATABASE_URL, ADMIN_PASSWORD, ADMIN_USERNAME
from datetime import datetime
import asyncpg
from auth import login_with_credentials
from client import extend_client_key_admin
from handlers.admin.admin_panel import back_to_admin_menu
from client import delete_client
router = Router()
class UserEditorState(StatesGroup):
waiting_for_tg_id = State()
displaying_user_info = State()
waiting_for_new_balance = State()
waiting_for_key_name = State()
waiting_for_expiry_time = State()
@router.callback_query(lambda c: c.data == "search_by_tg_id")
async def prompt_tg_id(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("Введите tg_id клиента:")
await state.set_state(UserEditorState.waiting_for_tg_id)
@router.message(UserEditorState.waiting_for_tg_id, F.text.isdigit())
async def handle_tg_id_input(message: types.Message, state: FSMContext):
tg_id = int(message.text)
conn = await asyncpg.connect(DATABASE_URL)
try:
balance = await conn.fetchval("SELECT balance FROM connections WHERE tg_id = $1", tg_id)
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)
if balance is None:
await message.reply("Пользователь с указанным tg_id не найден.")
await state.clear()
return
key_buttons = [
[InlineKeyboardButton(text=email, callback_data=f"edit_key_{email}")]
for email, in key_records
]
keyboard = InlineKeyboardMarkup(inline_keyboard=[
*key_buttons,
[InlineKeyboardButton(text="📝 Изменить баланс", callback_data=f"change_balance_{tg_id}")],
[InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")]
])
user_info = (
f"Информация о пользователе:\n"
f"Баланс: <b>{balance}</b>\n"
f"Количество рефералов: <b>{referral_count}</b>\n"
f"Ключи (для редактирования нажмите на ключ):"
)
await message.reply(user_info, reply_markup=keyboard, parse_mode="HTML")
await state.set_state(UserEditorState.displaying_user_info)
finally:
await conn.close()
@router.callback_query(lambda c: c.data.startswith('change_balance_'))
async def process_balance_change(callback_query: CallbackQuery, state: FSMContext):
tg_id = int(callback_query.data.split('_')[2])
await state.update_data(tg_id=tg_id)
await callback_query.message.edit_text("Введите новую сумму баланса:")
await callback_query.answer()
await state.set_state(UserEditorState.waiting_for_new_balance)
@router.message(UserEditorState.waiting_for_new_balance)
async def handle_new_balance_input(message: types.Message, state: FSMContext):
if not message.text.isdigit() or int(message.text) < 0:
await message.reply("Пожалуйста, введите корректную сумму для изменения баланса.")
return
new_balance = int(message.text)
user_data = await state.get_data()
tg_id = user_data.get('tg_id')
conn = await asyncpg.connect(DATABASE_URL)
try:
await conn.execute("UPDATE connections SET balance = $1 WHERE tg_id = $2", new_balance, tg_id)
response_message = f"Баланс успешно изменен на <b>{new_balance}</b>."
back_button = InlineKeyboardButton(text="Назад в меню админа", callback_data="back_to_user_editor")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML")
finally:
await conn.close()
await state.clear()
@router.callback_query(lambda c: c.data.startswith('edit_key_'))
async def process_key_edit(callback_query: CallbackQuery):
email = callback_query.data.split('_', 2)[2]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('''
SELECT k.key, k.expiry_time, k.server_id
FROM keys k
WHERE k.email = $1
''', email)
if record:
key = record['key']
expiry_time = record['expiry_time']
server_id = record['server_id']
server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер')
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime('%d %B %Y года')
response_message = (
f"Ключ: <pre>{key}</pre>\n"
f"Дата истечения: <b>{formatted_expiry_date}</b>\n"
f"{days_left_message}\n"
f"Сервер: <b>{server_name}</b>"
)
change_expiry_button = types.InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}')
delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}')
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[change_expiry_button, delete_button],
[InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")] # Кнопка "Назад"
]
)
await callback_query.message.edit_text(response_message, reply_markup=keyboard, parse_mode="HTML")
else:
await callback_query.message.edit_text("<b>Информация о ключе не найдена.</b>", parse_mode="HTML")
finally:
await conn.close()
except Exception as e:
await handle_error(callback_query.from_user.id, callback_query, f"Ошибка при получении информации о ключе: {e}")
await callback_query.answer()
@router.callback_query(lambda c: c.data == "search_by_key_name")
async def prompt_key_name(callback_query: CallbackQuery, state: FSMContext):
await callback_query.message.edit_text("Введите имя ключа:")
await state.set_state(UserEditorState.waiting_for_key_name)
@router.message(UserEditorState.waiting_for_key_name)
async def handle_key_name_input(message: types.Message, state: FSMContext):
key_name = message.text
conn = await asyncpg.connect(DATABASE_URL)
try:
user_records = await conn.fetch('''
SELECT c.tg_id, c.balance, k.email, k.key, k.expiry_time, k.server_id
FROM connections c
JOIN keys k ON c.tg_id = k.tg_id
WHERE k.email = $1
''', key_name)
if not user_records:
await message.reply("Пользователь с указанным именем ключа не найден.")
await state.clear()
return
response_messages = []
key_buttons = []
for record in user_records:
tg_id = record['tg_id']
balance = record['balance']
email = record['email']
key = record['key']
expiry_time = record['expiry_time']
server_id = record['server_id']
server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер')
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime('%d %B %Y')
response_messages.append(
f"Ключ: <pre>{key}</pre>\n"
f"Дата истечения: <b>{expiry_date}</b>\n"
f"Баланс пользователя: <b>{balance}</b>\n"
f"Сервер: <b>{server_name}</b>"
)
change_expiry_button = InlineKeyboardButton(text='⏳ Изменить время истечения', callback_data=f'change_expiry|{email}')
delete_button = InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key_admin|{email}')
key_buttons.append([change_expiry_button, delete_button])
key_buttons.append([InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")])
keyboard = InlineKeyboardMarkup(inline_keyboard=key_buttons)
await message.reply("\n".join(response_messages), reply_markup=keyboard, parse_mode="HTML")
finally:
await conn.close()
await state.clear()
@router.callback_query(lambda c: c.data.startswith('change_expiry|'))
async def prompt_expiry_change(callback_query: CallbackQuery, state: FSMContext):
email = callback_query.data.split('|')[1]
await callback_query.message.edit_text(
f"Введите новое время истечения для ключа <b>{email}</b> в формате <code>YYYY-MM-DD HH:MM:SS</code>:",
parse_mode="HTML"
)
await state.update_data(email=email)
await state.set_state(UserEditorState.waiting_for_expiry_time)
@router.message(UserEditorState.waiting_for_expiry_time)
async def handle_expiry_time_input(message: types.Message, state: FSMContext):
user_data = await state.get_data()
email = user_data.get('email')
if not email:
await message.reply("Email не найден в состоянии.")
await state.clear()
return
try:
expiry_time_str = message.text
expiry_time = int(datetime.strptime(expiry_time_str, '%Y-%m-%d %H:%M:%S').timestamp() * 1000)
client_id = await get_client_id_by_email(email)
if client_id is None:
await message.reply(f"Клиент с email {email} не найден.")
await state.clear()
return
await update_key_expiry(client_id, expiry_time)
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT server_id FROM keys WHERE client_id = $1', client_id)
if not record:
await message.reply("Клиент не найден в базе данных.")
await state.clear()
return
server_id = record['server_id']
tg_id = await get_tg_id_by_client_id(client_id)
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
print(f"Попытка обновить панель для server_id: {server_id}, tg_id: {tg_id}, client_id: {client_id}, email: {email}, expiryTime: {expiry_time}")
success = await extend_client_key_admin(session, server_id, tg_id, client_id, email, expiry_time)
print(f"Статус обновления панели: {'Успешно' if success else 'Не удалось'}")
if success:
response_message = (
f"Время истечения ключа для клиента {client_id} ({email}) успешно обновлено и синхронизировано с панелью."
)
else:
response_message = (
f"Время истечения ключа для клиента {client_id} ({email}) обновлено, но не удалось синхронизировать с панелью."
)
back_button = InlineKeyboardButton(text="Назад", callback_data="back_to_user_editor")
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await message.reply(response_message, reply_markup=keyboard, parse_mode="HTML")
finally:
await conn.close()
except ValueError:
await message.reply("Пожалуйста, используйте формат: YYYY-MM-DD HH:MM:SS.")
except Exception as e:
await message.reply(f"Произошла ошибка: {e}")
await state.clear()
@router.callback_query(lambda c: c.data.startswith('delete_key_admin|'))
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
email = callback_query.data.split('|')[1]
conn = await asyncpg.connect(DATABASE_URL)
try:
client_id = await conn.fetchval('SELECT client_id FROM keys WHERE email = $1', email)
if client_id is None:
await bot.edit_message_text("Ключ не найден.", chat_id=tg_id, message_id=callback_query.message.message_id)
return
confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete_admin|{client_id}')],
[types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')]
])
await bot.edit_message_text("<b>Вы уверены, что хотите удалить ключ?</b>", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard, parse_mode="HTML")
finally:
await conn.close()
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('confirm_delete_admin|'))
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email, server_id FROM keys WHERE client_id = $1', client_id)
if record:
email = record['email']
server_id = record['server_id']
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success = await delete_client(session, server_id, client_id)
if success:
await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id)
response_message = "Ключ был успешно удален."
else:
response_message = "Ошибка при удалении клиента через API."
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(f"Ошибка при удалении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
await callback_query.answer()
@router.callback_query(lambda c: c.data == "back_to_user_editor")
async def back_to_user_editor(callback_query: CallbackQuery):
await back_to_admin_menu(callback_query)
async def handle_error(tg_id, callback_query, message):
await bot.edit_message_text(message, chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML")
+3 -135
View File
@@ -1,149 +1,17 @@
from aiogram import F, Router, types
from aiogram import Router, types
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
import asyncpg
from bot import bot
from config import ADMIN_ID, DATABASE_URL
from handlers.payment.pay import ReplenishBalanceState, process_custom_amount_input
from handlers.profile import process_callback_view_profile
from handlers.start import start_command
from handlers.texts import TRIAL
from handlers.admin.admin import cmd_add_balance
from handlers.keys.key_management import handle_key_name_input
from aiogram.types import Message
router = Router()
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
@router.message(Command('backup'))
async def backup_command(message: Message):
if message.from_user.id != ADMIN_ID:
await message.answer("У вас нет прав для выполнения этой команды.")
return
from backup import backup_database
await message.answer("Запускаю бэкап базы данных...")
await backup_database()
await message.answer("Бэкап завершен и отправлен админу.")
@router.message(Command('start'))
@router.message(Command("start"))
async def handle_start(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command('add_balance'))
async def handle_add_balance(message: types.Message, state: FSMContext):
await cmd_add_balance(message)
@router.message(Command('menu'))
@router.message(Command("menu"))
async def handle_menu(message: types.Message, state: FSMContext):
await start_command(message)
@router.message(Command('send_trial'))
async def handle_send_trial_command(message: types.Message, state: FSMContext):
# Проверка на администратора
if message.from_user.id != ADMIN_ID:
await message.reply("У вас нет доступа к этой команде.")
return
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch('''
SELECT tg_id FROM connections WHERE trial = 0
''')
if records:
for record in records:
tg_id = record['tg_id']
trial_message = TRIAL
try:
await bot.send_message(chat_id=tg_id, text=trial_message)
except Exception as e:
if "Forbidden: bot was blocked by the user" in str(e):
print(f"Бот заблокирован пользователем с tg_id: {tg_id}")
else:
print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}")
await message.answer("Сообщения о пробном периоде отправлены всем пользователям с не использованным ключом.")
else:
await message.answer("Нет пользователей с не использованными пробными ключами.")
finally:
await conn.close()
except Exception as e:
await message.answer(f"Ошибка при отправке сообщений: {e}")
@router.message(Command('send_to_all'))
async def send_message_to_all_clients(message: types.Message, state: FSMContext, from_panel=False):
if not from_panel and message.from_user.id != ADMIN_ID:
await message.answer("У вас нет прав для выполнения этой команды.")
return
await message.answer("Введите текст сообщения, который вы хотите отправить всем клиентам:")
await state.set_state(Form.waiting_for_message)
@router.message(Form.waiting_for_message)
async def process_message_to_all(message: types.Message, state: FSMContext):
text_message = message.text
try:
conn = await asyncpg.connect(DATABASE_URL)
tg_ids = await conn.fetch('SELECT tg_id FROM connections')
for record in tg_ids:
tg_id = record['tg_id']
try:
await bot.send_message(chat_id=tg_id, text=text_message)
except Exception as e:
print(f"Ошибка при отправке сообщения пользователю {tg_id}: {e}. Пропускаем этого пользователя.")
await message.answer("Сообщение было отправлено всем клиентам.")
except Exception as e:
print(f"Ошибка при подключении к базе данных: {e}")
await message.answer("Произошла ошибка при отправке сообщения.")
finally:
await conn.close()
await state.clear()
@router.message()
async def handle_text(message: types.Message, state: FSMContext):
current_state = await state.get_state()
if message.text in ["/send_to_all"]:
await send_message_to_all_clients(message, state)
return
if message.text == "Мой профиль":
callback_query = types.CallbackQuery(
id="1",
from_user=message.from_user,
chat_instance='',
data='view_profile',
message=message
)
await process_callback_view_profile(callback_query, state)
return
if current_state == ReplenishBalanceState.entering_custom_amount.state:
await process_custom_amount_input(message, state)
return
if current_state == Form.waiting_for_key_name.state:
await handle_key_name_input(message, state)
return
if message.text == "/backup":
await backup_command(message)
return
elif current_state is None:
await start_command(message)
+128
View File
@@ -0,0 +1,128 @@
from datetime import datetime
import asyncpg
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import DATABASE_URL
from database import update_balance
from logger import logger
class CouponActivationState(StatesGroup):
waiting_for_coupon_code = State()
router = Router()
@router.callback_query(F.data == "activate_coupon")
async def handle_activate_coupon(
callback_query: types.CallbackQuery, state: FSMContext
):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
await callback_query.message.answer(
"<b>Введите код купона:</b>\n\n"
"Пожалуйста, введите действующий код купона, который вы хотите активировать.",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await state.set_state(CouponActivationState.waiting_for_coupon_code)
await callback_query.answer()
@router.message(CouponActivationState.waiting_for_coupon_code)
async def process_coupon_code(message: types.Message, state: FSMContext):
try:
await message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
coupon_code = message.text.strip()
activation_result = await activate_coupon(message.from_user.id, coupon_code)
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
)
markup = builder.as_markup()
await message.answer(activation_result, reply_markup=markup, parse_mode="HTML")
await state.clear()
async def activate_coupon(user_id: int, coupon_code: str):
"""Функция для активации купона"""
conn = await asyncpg.connect(DATABASE_URL)
try:
coupon_record = await conn.fetchrow(
"""
SELECT id, usage_limit, usage_count, is_used, amount
FROM coupons
WHERE code = $1 AND (usage_count < usage_limit OR usage_limit = 0) AND is_used = FALSE
""",
coupon_code,
)
if not coupon_record:
return "<b>❌ Купон не найден</b> или его использование ограничено. Пожалуйста, проверьте код и попробуйте снова."
usage_exists = await conn.fetchrow(
"""
SELECT 1 FROM coupon_usages WHERE coupon_id = $1 AND user_id = $2
""",
coupon_record["id"],
user_id,
)
if usage_exists:
return "<b>❌ Вы уже активировали этот купон.</b> Купоны могут быть активированы только один раз."
coupon_amount = coupon_record["amount"]
async with conn.transaction():
await conn.execute(
"""
UPDATE coupons
SET usage_count = usage_count + 1,
is_used = CASE WHEN usage_count + 1 >= usage_limit AND usage_limit > 0 THEN TRUE ELSE FALSE END
WHERE id = $1
""",
coupon_record["id"],
)
await conn.execute(
"""
INSERT INTO coupon_usages (coupon_id, user_id, used_at)
VALUES ($1, $2, $3)
""",
coupon_record["id"],
user_id,
datetime.utcnow(),
)
await update_balance(user_id, coupon_amount)
return f"<b>✅ Купон успешно активирован!</b>\n\nНа ваш баланс добавлено <b>{coupon_amount} рублей</b>."
except Exception as e:
logger.error(f"Ошибка при активации купона: {e}")
return (
"<b>⚠️ Произошла ошибка при активации купона.</b>\nПопробуйте ещё раз позже."
)
finally:
await conn.close()
+145
View File
@@ -0,0 +1,145 @@
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import RUB_TO_XTR
from logger import logger
class DonateState(StatesGroup):
entering_donate_amount = State()
waiting_for_donate_confirmation = State()
waiting_for_donate_payment = State()
router = Router()
@router.callback_query(F.data == "donate")
async def process_donate(callback_query: types.CallbackQuery, state: FSMContext):
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести сумму доната", callback_data="enter_custom_donate_amount"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
await bot.send_message(
chat_id=callback_query.from_user.id,
text="🌟 Поддержите наш проект!\n\n"
"Каждый донат помогает развивать и улучшать сервис. "
"Мы ценим вашу поддержку и работаем над тем, чтобы сделать наш продукт еще лучше. 💡",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "enter_custom_donate_amount")
async def process_enter_donate_amount(
callback_query: types.CallbackQuery, state: FSMContext
):
await callback_query.message.edit_text(f"💸 Введите сумму доната в рублях:")
await state.set_state(DonateState.entering_donate_amount)
await callback_query.answer()
@router.message(DonateState.entering_donate_amount)
async def process_donate_amount_input(message: types.Message, state: FSMContext):
try:
await message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
await message.answer(
f"Сумма доната должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(DonateState.waiting_for_donate_confirmation)
try:
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="Задонатить", pay=True))
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="donate"))
await message.answer_invoice(
title=f"Донат проекту {amount} рублей",
description="Спасибо за вашу поддержку!",
prices=[LabeledPrice(label="Донат", amount=int(amount // RUB_TO_XTR))],
provider_token="",
payload=f"{amount}_donate",
currency="XTR",
reply_markup=builder.as_markup(),
)
await state.set_state(DonateState.waiting_for_donate_payment)
except Exception as e:
logger.error(f"Ошибка при создании доната: {e}")
await message.answer("Произошла ошибка при создании доната.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
@router.pre_checkout_query(DonateState.waiting_for_donate_payment)
async def on_pre_checkout_query(pre_checkout_query: PreCheckoutQuery):
await pre_checkout_query.answer(ok=True)
@router.message(F.successful_payment, DonateState.waiting_for_donate_payment)
async def on_successful_donate(message: types.Message, state: FSMContext):
try:
user_id = int(message.from_user.id)
amount = float(message.successful_payment.invoice_payload.split("_")[0])
logger.debug(f"Donate succeeded for user_id: {user_id}, amount: {amount}")
state_data = await state.get_data()
previous_message_id = state_data.get("last_message_id")
if previous_message_id:
try:
await bot.delete_message(
chat_id=user_id, message_id=previous_message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(
text="Вернуться в профиль", callback_data="view_profile"
)
)
sent_message = await bot.send_message(
chat_id=user_id,
text=f"🙏 Спасибо за донат {amount} рублей! Ваша поддержка очень важна для нас. 💖",
reply_markup=builder.as_markup(),
)
await state.update_data(last_message_id=sent_message.message_id)
await state.clear()
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
except Exception as e:
logger.error(f"Произошла ошибка при обработке доната: {e}")
+56 -13
View File
@@ -1,34 +1,77 @@
import os
from aiogram import types
from aiogram.types import (BufferedInputFile, InlineKeyboardButton,
InlineKeyboardMarkup)
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile, InlineKeyboardButton, InlineKeyboardMarkup
from bot import bot
from config import CONNECT_WINDOWS, SUPPORT_CHAT_URL
from handlers.texts import INSTRUCTION_PC, INSTRUCTIONS, KEY_MESSAGE
from logger import logger
router = Router()
from handlers.texts import INSTRUCTIONS
async def send_instructions(callback_query: types.CallbackQuery):
await callback_query.message.delete()
instructions_message = (
INSTRUCTIONS
)
instructions_message = INSTRUCTIONS
image_path = os.path.join(os.path.dirname(__file__), 'instructions.jpg')
image_path = os.path.join(os.path.dirname(__file__), "instructions.jpg")
if not os.path.isfile(image_path):
await callback_query.message.answer("Файл изображения не найден.")
await callback_query.answer()
return
back_button = InlineKeyboardButton(text='🔙 Назад', callback_data='back_to_main')
back_button = InlineKeyboardButton(
text="⬅️ Вернуться в профиль", callback_data="view_profile"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[back_button]])
with open(image_path, 'rb') as image_from_buffer:
with open(image_path, "rb") as image_from_buffer:
await callback_query.message.answer_photo(
BufferedInputFile(image_from_buffer.read(), filename="instructions.jpg"),
caption=instructions_message,
parse_mode='Markdown',
reply_markup=keyboard
parse_mode="Markdown",
reply_markup=keyboard,
)
await callback_query.answer()
@router.callback_query(F.data.startswith("connect_pc|"))
async def process_connect_pc(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
key = callback_query.data.split("|")[1]
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
key_message = KEY_MESSAGE.format(key)
instruction_message = f"{key_message}{INSTRUCTION_PC}"
connect_windows_button = types.InlineKeyboardButton(
text="💻 Подключить Windows", url=f"{CONNECT_WINDOWS}{key}"
)
support_button = types.InlineKeyboardButton(
text="🆘 Поддержка", url=f"{SUPPORT_CHAT_URL}"
)
back_button = types.InlineKeyboardButton(
text="🔙 Назад в профиль", callback_data="view_profile"
)
inline_keyboard = [[connect_windows_button], [support_button], [back_button]]
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
await bot.send_message(
tg_id, instruction_message, reply_markup=keyboard, parse_mode="HTML"
)
await callback_query.answer()
+170 -114
View File
@@ -1,145 +1,163 @@
import asyncio
import uuid
from datetime import datetime, timedelta
from bot import dp, bot
import asyncpg
from aiogram import F, Router
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (CallbackQuery, InlineKeyboardButton,
InlineKeyboardMarkup, Message)
from aiogram.types import CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message
from auth import login_with_credentials, link_subscription
from client import add_client
from config import (ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL,
SERVERS, APP_URL)
from bot import bot, dp
from config import CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK
from database import add_connection, get_balance, store_key, update_balance
from handlers.instructions.instructions import send_instructions
from handlers.keys.key_utils import create_key_on_cluster
from handlers.profile import process_callback_view_profile
from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, key_message_success
from handlers.utils import sanitize_key_name
from handlers.texts import KEY, KEY_TRIAL, NULL_BALANCE, RENEWAL_PLANS, key_message_success
from handlers.utils import get_least_loaded_cluster, sanitize_key_name
from logger import logger
router = Router()
class Form(StatesGroup):
waiting_for_server_selection = State()
waiting_for_key_name = State()
viewing_profile = State()
waiting_for_message = State()
@dp.callback_query(F.data == 'create_key')
@dp.callback_query(F.data == "create_key")
async def process_callback_create_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
server_buttons = []
conn = await asyncpg.connect(DATABASE_URL)
try:
for server_id, server in SERVERS.items():
count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id)
percent_full = (count / 60) * 100 if count <= 60 else 100
server_name = f"{server['name']} ({percent_full:.1f}%)"
server_buttons.append([InlineKeyboardButton(text=server_name, callback_data=f'select_server|{server_id}')])
finally:
await conn.close()
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='view_profile')
server_buttons.append([button_back])
await callback_query.message.delete()
await bot.send_message(
chat_id=tg_id,
text="<b>⚙️ Выберите сервер для создания ключа:</b>",
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(inline_keyboard=server_buttons)
)
await state.set_state(Form.waiting_for_server_selection)
server_id = "все сервера"
await state.update_data(selected_server_id=server_id)
await select_server(callback_query, state)
await callback_query.answer()
@dp.callback_query(F.data.startswith('select_server|'))
async def select_server(callback_query: CallbackQuery, state: FSMContext):
server_id = callback_query.data.split('|')[1]
await state.update_data(selected_server_id=server_id)
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', callback_query.from_user.id)
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1",
callback_query.from_user.id,
)
finally:
await conn.close()
trial_status = existing_connection['trial'] if existing_connection else 0
trial_status = existing_connection["trial"] if existing_connection else 0
if trial_status == 1:
await callback_query.message.edit_text(
KEY,
await bot.send_message(
chat_id=callback_query.from_user.id,
text=KEY,
parse_mode="HTML",
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='✅ Да, создать новый ключ', callback_data='confirm_create_new_key')],
[InlineKeyboardButton(text='↩️ Назад', callback_data='cancel_create_key')]
])
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="✅ Да, подключить новое устройство",
callback_data="confirm_create_new_key",
)
],
[
InlineKeyboardButton(
text="↩️ Назад", callback_data="cancel_create_key"
)
],
]
),
)
await state.update_data(creating_new_key=True)
else:
await callback_query.message.edit_text(
KEY_TRIAL,
parse_mode="HTML"
await bot.send_message(
chat_id=callback_query.from_user.id, text=KEY_TRIAL, parse_mode="HTML"
)
await state.set_state(Form.waiting_for_key_name)
await callback_query.answer()
@dp.callback_query(F.data == 'confirm_create_new_key')
@dp.callback_query(F.data == "confirm_create_new_key")
async def confirm_create_new_key(callback_query: CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
data = await state.get_data()
server_id = data.get('selected_server_id')
logger.info(f"User {tg_id} confirmed creation of a new key.")
balance = await get_balance(tg_id)
if balance < 100:
replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await callback_query.message.edit_text(
NULL_BALANCE,
reply_markup=keyboard
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await callback_query.message.edit_text(NULL_BALANCE, reply_markup=keyboard)
await state.clear()
return
await callback_query.message.edit_text("🔑 Пожалуйста, введите имя нового ключа:")
logger.info(f"Balance for user {tg_id} is sufficient. Asking for device name.")
await callback_query.message.edit_text(
"🔑 Пожалуйста, введите имя подключаемого устройства:"
)
await state.set_state(Form.waiting_for_key_name)
logger.info(f"State set to waiting_for_key_name for user {tg_id}")
await state.update_data(creating_new_key=True)
await callback_query.answer()
@dp.callback_query(F.data == 'cancel_create_key')
@dp.callback_query(F.data == "cancel_create_key")
async def cancel_create_key(callback_query: CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
await callback_query.answer()
@router.message(Form.waiting_for_key_name)
async def handle_key_name_input(message: Message, state: FSMContext):
tg_id = message.from_user.id
key_name = sanitize_key_name(message.text)
logger.info(f"User {tg_id} is attempting to create a key with the name: {key_name}")
if not key_name:
await message.bot.send_message(tg_id, "📝 Пожалуйста, назовите ключ устройства на английском языке.")
await message.bot.send_message(
tg_id, "📝 Пожалуйста, назовите устройство на английском языке."
)
logger.warning(f"User {tg_id} entered an invalid key name: {key_name}")
return
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_key = await conn.fetchrow('SELECT * FROM keys WHERE email = $1', key_name.lower())
logger.info(
f"Checking if key name '{key_name}' already exists in the database."
)
existing_key = await conn.fetchrow(
"SELECT * FROM keys WHERE email = $1", key_name.lower()
)
if existing_key:
await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.")
await message.bot.send_message(
tg_id,
"❌ Упс! Это имя уже используется. Выберите другое уникальное название для ключа.",
)
logger.warning(
f"Key name '{key_name}' already exists in the database for user {tg_id}."
)
await state.set_state(Form.waiting_for_key_name)
return
finally:
await conn.close()
data = await state.get_data()
creating_new_key = data.get('creating_new_key', False)
server_id = data.get('selected_server_id')
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
client_id = str(uuid.uuid4())
email = key_name.lower()
current_time = datetime.utcnow()
@@ -147,91 +165,129 @@ async def handle_key_name_input(message: Message, state: FSMContext):
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow('SELECT trial FROM connections WHERE tg_id = $1', tg_id)
logger.info(f"Checking trial status for user {tg_id}.")
existing_connection = await conn.fetchrow(
"SELECT trial FROM connections WHERE tg_id = $1", tg_id
)
finally:
await conn.close()
trial_status = existing_connection['trial'] if existing_connection else 0
trial_status = existing_connection["trial"] if existing_connection else 0
if trial_status == 0:
expiry_time = current_time + timedelta(days=1, hours=3)
logger.info(f"Assigned 1-day trial to user {tg_id}.")
else:
balance = await get_balance(tg_id)
if balance < 100:
replenish_button = InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')
if balance < RENEWAL_PLANS["1"]["price"]:
replenish_button = InlineKeyboardButton(
text="Перейти в профиль", callback_data="view_profile"
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[[replenish_button]])
await message.bot.send_message(tg_id, "❗️ Недостаточно средств на балансе для создания нового ключа.", reply_markup=keyboard)
await message.bot.send_message(
tg_id,
"💳 Недостаточно средств для создания подписки на новое устройство. Пополните баланс в личном кабинете.",
reply_markup=keyboard,
)
logger.warning(f"User {tg_id} has insufficient funds for key creation.")
await state.clear()
return
await update_balance(tg_id, -100)
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
expiry_time = current_time + timedelta(days=30, hours=3)
logger.info(f"User {tg_id} balance deducted for key creation.")
expiry_timestamp = int(expiry_time.timestamp() * 1000)
public_link = f"{PUBLIC_LINK}{email}"
logger.info(f"Generated public link for the key: {public_link}")
button_profile = InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
button_iphone = InlineKeyboardButton(
text="🍏 Подключить", url=f"{CONNECT_IOS}{public_link}"
)
button_android = InlineKeyboardButton(
text="🤖 Подключить",
url=f"{CONNECT_ANDROID}{public_link}",
)
button_download_ios = InlineKeyboardButton(text="🍏 Скачать", url=DOWNLOAD_IOS)
button_download_android = InlineKeyboardButton(
text="🤖 Скачать",
url=DOWNLOAD_ANDROID,
)
keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[button_download_ios, button_download_android],
[button_iphone, button_android],
[button_profile],
]
)
remaining_time = expiry_time - current_time
days = remaining_time.days
key_message = key_message_success(public_link, f"⏳ Осталось дней: {days} 📅")
logger.info(f"Sending key message to user {tg_id} with the public link.")
await message.bot.send_message(
tg_id, key_message, parse_mode="HTML", reply_markup=keyboard
)
try:
response = await add_client(session, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0, expiry_time=expiry_timestamp, enable=True, flow="xtls-rprx-vision")
if not response.get("success", True):
error_msg = response.get("msg", "Неизвестная ошибка.")
if "Duplicate email" in error_msg:
await message.bot.send_message(tg_id, "❌ Это имя уже используется. Пожалуйста, выберите другое имя для ключа.")
await state.set_state(Form.waiting_for_key_name)
return
else:
raise Exception(error_msg)
least_loaded_cluster = await get_least_loaded_cluster()
connection_link = await link_subscription(email, server_id)
tasks = []
tasks.append(
asyncio.create_task(
create_key_on_cluster(
least_loaded_cluster,
tg_id,
client_id,
email,
expiry_timestamp,
)
)
)
await asyncio.gather(*tasks)
conn = await asyncpg.connect(DATABASE_URL)
try:
existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id)
logger.info(f"Updating trial status for user {tg_id} in the database.")
existing_connection = await conn.fetchrow(
"SELECT * FROM connections WHERE tg_id = $1", tg_id
)
if existing_connection:
await conn.execute('UPDATE connections SET trial = 1 WHERE tg_id = $1', tg_id)
await conn.execute(
"UPDATE connections SET trial = 1 WHERE tg_id = $1", tg_id
)
else:
await add_connection(tg_id, 0, 1)
finally:
await conn.close()
await store_key(tg_id, client_id, email, expiry_timestamp, connection_link, server_id)
remaining_time = expiry_time - current_time
days = remaining_time.days
hours, remainder = divmod(remaining_time.seconds, 3600)
minutes, _ = divmod(remainder, 60)
remaining_time_message = f"Оставшееся время ключа: {days} день"
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
button_iphone = InlineKeyboardButton(
text='🍏IPhone',
url=f'{APP_URL}/?url=v2raytun://import/{connection_link}'
logger.info(f"Storing key for user {tg_id} in the database.")
await store_key(
tg_id, client_id, email, expiry_timestamp, public_link, least_loaded_cluster
)
button_android = InlineKeyboardButton(
text='🤖Android',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={connection_link}'
)
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[button_iphone, button_android],
[button_profile]
])
key_message = key_message_success(connection_link, remaining_time_message)
await message.bot.send_message(tg_id, key_message, parse_mode="HTML", reply_markup=keyboard)
except Exception as e:
logger.error(f"Error while creating the key for user {tg_id}: {e}")
await message.bot.send_message(tg_id, f"❌ Ошибка при создании ключа: {e}")
await state.clear()
@dp.callback_query(F.data == 'instructions')
async def handle_instructions(callback_query: CallbackQuery):
await send_instructions(callback_query)
@dp.callback_query(F.data == 'back_to_main')
@dp.callback_query(F.data == "instructions")
async def handle_instructions(callback_query: CallbackQuery):
await send_instructions(callback_query)
@dp.callback_query(F.data == "back_to_main")
async def handle_back_to_main(callback_query: CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
await callback_query.answer()
+165
View File
@@ -0,0 +1,165 @@
import asyncio
import asyncpg
from py3xui import AsyncApi
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB
from logger import logger
async def create_key_on_cluster(cluster_id, tg_id, client_id, email, expiry_timestamp):
try:
tasks = []
cluster = CLUSTERS.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
for server_info in cluster.values():
xui = AsyncApi(
server_info["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
conn = await asyncpg.connect(DATABASE_URL)
existing_key = await conn.fetchrow(
"SELECT 1 FROM keys WHERE email = $1", email
)
if existing_key:
raise ValueError(f"Email {email} уже существует в базе данных.")
tasks.append(
add_client(
xui,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=TOTAL_GB,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
)
)
await conn.close()
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при создании ключа: {e}")
raise e
async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, total_gb):
"""
Функция для продления срока действия ключа на всех серверах в кластере
и обновления объема трафика.
"""
try:
cluster = CLUSTERS.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
tasks = []
for server_info in cluster.values():
xui = AsyncApi(
server_info["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
tasks.append(
extend_client_key(xui, email, new_expiry_time, client_id, total_gb)
)
await asyncio.gather(*tasks)
except Exception as e:
logger.error(
f"Не удалось продлить ключ {client_id} в кластере {cluster_id}: {e}"
)
raise e
async def delete_key_from_db(client_id):
"""Удаление ключа из базы данных"""
try:
conn = await asyncpg.connect(DATABASE_URL)
await conn.execute("DELETE FROM keys WHERE client_id = $1", client_id)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id} из базы данных: {e}")
finally:
await conn.close()
async def delete_key_from_cluster(cluster_id, email, client_id):
"""Удаление ключа с серверов в кластере"""
try:
cluster = CLUSTERS.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
tasks = []
for server_info in cluster.values():
xui = AsyncApi(
server_info["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
tasks.append(delete_client(xui, email, client_id))
await asyncio.gather(*tasks)
except Exception as e:
logger.error(
f"Не удалось удалить ключ {client_id} в кластере {cluster_id}: {e}"
)
raise e
async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id):
try:
cluster = CLUSTERS.get(cluster_id)
if not cluster:
raise ValueError(f"Кластер с ID {cluster_id} не найден.")
tasks = []
for server_info in cluster.values():
xui = AsyncApi(
server_info["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
tasks.append(
add_client(
xui,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=TOTAL_GB,
expiry_time=expiry_time,
enable=True,
flow="xtls-rprx-vision",
)
)
await asyncio.gather(*tasks)
logger.info(
f"Ключ успешно обновлен для {client_id} на всех серверах в кластере {cluster_id}"
)
except Exception as e:
logger.error(
f"Ошибка при обновлении ключа на серверах кластера {cluster_id} для {client_id}: {e}"
)
raise e
+572 -299
View File
@@ -1,73 +1,129 @@
import asyncio
import locale
import os
from datetime import datetime, timedelta
import asyncpg
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.types import BufferedInputFile
from auth import login_with_credentials, link_subscription
from bot import bot
from client import add_client, delete_client, extend_client_key
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, SERVERS, APP_URL
from database import get_balance, update_balance
from handlers.texts import NO_KEYS
from handlers.texts import key_message, key_relocated
from handlers.texts import RENEWAL_PLANS, INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, SUCCESS_RENEWAL_MSG, ERROR_RENEWAL_MSG, PLAN_SELECTION_MSG
from config import CLUSTERS, CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, PUBLIC_LINK, TOTAL_GB
from database import delete_key, get_balance, store_key, update_balance, update_key_expiry
from handlers.keys.key_utils import delete_key_from_cluster, delete_key_from_db, renew_key_in_cluster, update_key_on_cluster
from handlers.texts import INSUFFICIENT_FUNDS_MSG, KEY_NOT_FOUND_MSG, NO_KEYS, PLAN_SELECTION_MSG, RENEWAL_PLANS, SUCCESS_RENEWAL_MSG, key_message
from handlers.utils import get_least_loaded_cluster, handle_error
from logger import logger
locale.setlocale(locale.LC_TIME, 'ru_RU.UTF-8')
locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8")
router = Router()
@router.callback_query(lambda c: c.data == 'view_keys')
@router.callback_query(F.data == "view_keys")
async def process_callback_view_keys(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
records = await conn.fetch('''
records = await conn.fetch(
"""
SELECT email, client_id FROM keys WHERE tg_id = $1
''', tg_id)
""",
tg_id,
)
if records:
buttons = []
for record in records:
key_name = record['email']
client_id = record['client_id']
button = types.InlineKeyboardButton(text=f"🔑 {key_name}", callback_data=f'view_key|{key_name}|{client_id}')
key_name = record["email"]
client_id = record["client_id"]
button = types.InlineKeyboardButton(
text=f"🔑 {key_name}",
callback_data=f"view_key|{key_name}|{client_id}",
)
buttons.append([button])
back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')
back_button = types.InlineKeyboardButton(
text="🔙 Назад", callback_data="view_profile"
)
buttons.append([back_button])
inline_keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons)
response_message = (
"<b>Это ваши устройства:</b>\n\n"
"<i>Нажмите на имя устройства для управления его ключом.</i>"
"<b>🔑 Список ваших устройств</b>\n\n"
"<i>👇 Выберите устройство для управления подпиской:</i>"
)
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
image_path = os.path.join(os.path.dirname(__file__), "pic_keys.jpg")
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(
image_file.read(), filename="pic_keys.jpg"
),
caption=response_message,
parse_mode="HTML",
reply_markup=inline_keyboard,
)
else:
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=inline_keyboard,
parse_mode="HTML",
)
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=inline_keyboard,
parse_mode="HTML"
)
else:
response_message = NO_KEYS
create_key_button = types.InlineKeyboardButton(text=' Создать ключ', callback_data='create_key')
back_button = types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[create_key_button], [back_button]])
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=keyboard,
parse_mode="HTML"
response_message = NO_KEYS
create_key_button = types.InlineKeyboardButton(
text=" Создать подписку", callback_data="create_key"
)
back_button = types.InlineKeyboardButton(
text="🔙 Назад", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[create_key_button], [back_button]]
)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
image_path = os.path.join(os.path.dirname(__file__), "pic_keys.jpg")
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(
image_file.read(), filename="pic_keys.jpg"
),
caption=response_message,
parse_mode="HTML",
reply_markup=keyboard,
)
else:
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
finally:
await conn.close()
@@ -77,327 +133,544 @@ async def process_callback_view_keys(callback_query: types.CallbackQuery):
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('view_key|'))
@router.callback_query(F.data.startswith("view_key|"))
async def process_callback_view_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
key_name, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2]
key_name, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
)
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('''
SELECT k.key, k.expiry_time, k.server_id
record = await conn.fetchrow(
"""
SELECT k.expiry_time, k.server_id, k.key
FROM keys k
WHERE k.tg_id = $1 AND k.email = $2
''', tg_id, key_name)
""",
tg_id,
key_name,
)
if record:
key = record['key']
expiry_time = record['expiry_time']
server_id = record['server_id']
server_name = SERVERS.get(server_id, {}).get('name', 'Неизвестный сервер')
key = record["key"]
expiry_time = record["expiry_time"]
server_name = record["server_id"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
if time_left.total_seconds() <= 0:
days_left_message = "<b>Ключ истек.</b>"
days_left_message = (
"<b>🕒 Статус подписки:</b>\n🔴 Истекла\nОсталось часов: 0"
)
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
formatted_expiry_date = expiry_date.strftime('%d %B %Y года')
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = 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(
text="🤖 Скачать", url=DOWNLOAD_ANDROID
)
download_iphone_button = types.InlineKeyboardButton(
text="🍏 Скачать", url=DOWNLOAD_IOS
)
renew_button = types.InlineKeyboardButton(text='⏳ Продлить ключ', callback_data=f'renew_key|{client_id}')
instructions_button = types.InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions')
delete_button = types.InlineKeyboardButton(text='❌ Удалить ключ', callback_data=f'delete_key|{client_id}')
change_location_button = types.InlineKeyboardButton(text='🌍 Сменить локацию', callback_data=f'change_location|{client_id}')
back_button = types.InlineKeyboardButton(text='🔙 Назад в профиль', callback_data='view_profile')
connect_iphone_button = types.InlineKeyboardButton(
text="🍏 Подключить", url=f"{CONNECT_IOS}{key}"
)
connect_android_button = types.InlineKeyboardButton(
text="🤖 Подключить", url=f"{CONNECT_ANDROID}{key}"
)
connect_pc_button = types.InlineKeyboardButton(
text="💻 Windows/Linux", callback_data=f"connect_pc|{key}"
)
renew_button = types.InlineKeyboardButton(
text="⏳ Продлить", callback_data=f"renew_key|{client_id}"
)
delete_button = types.InlineKeyboardButton(
text="❌ Удалить", callback_data=f"delete_key|{client_id}"
)
back_button = types.InlineKeyboardButton(
text="🔙 Назад в профиль", callback_data="view_profile"
)
inline_keyboard = [
[download_iphone_button, download_android_button],
[connect_iphone_button, connect_android_button],
[connect_pc_button],
[renew_button, delete_button],
]
if not key.startswith(PUBLIC_LINK):
update_subscription_button = types.InlineKeyboardButton(
text="🔄 Обновить подписку",
callback_data=f"update_subscription|{client_id}",
)
inline_keyboard.append([update_subscription_button])
inline_keyboard.append([back_button])
keyboard = types.InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
image_path = os.path.join(os.path.dirname(__file__), "pic_view.jpg")
if not os.path.isfile(image_path):
await bot.send_message(tg_id, "Файл изображения не найден.")
return
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(
image_file.read(), filename="pic_view.jpg"
),
caption=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
else:
await bot.send_message(
chat_id=tg_id,
text="<b>Информация о подписке не найдена.</b>",
parse_mode="HTML",
)
finally:
await conn.close()
except Exception as e:
await handle_error(
tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}"
)
await callback_query.answer()
@router.callback_query(F.data.startswith("update_subscription|"))
async def process_callback_update_subscription(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"""
SELECT k.key, k.expiry_time, k.email, k.server_id
FROM keys k
WHERE k.tg_id = $1 AND k.client_id = $2
""",
tg_id,
client_id,
)
if record:
expiry_time = record["expiry_time"]
email = record["email"]
public_link = f"{PUBLIC_LINK}{email}"
try:
await conn.execute(
"""
DELETE FROM keys
WHERE tg_id = $1 AND client_id = $2
""",
tg_id,
client_id,
)
except Exception as delete_error:
await bot.send_message(
tg_id, f"Ошибка при удалении старой подписки: {delete_error}"
)
return
least_loaded_cluster_id = await get_least_loaded_cluster()
tasks = []
tasks.append(
update_key_on_cluster(
tg_id, client_id, email, expiry_time, least_loaded_cluster_id
)
)
await asyncio.gather(*tasks)
await store_key(
tg_id,
client_id,
email,
expiry_time,
public_link,
server_id=least_loaded_cluster_id,
)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
response_message = f"Ваша подписка {email} обновлена!"
back_button = types.InlineKeyboardButton(
text="🔙 Назад в профиль", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.send_message(
tg_id, response_message, reply_markup=keyboard, parse_mode="HTML"
)
else:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await bot.send_message(
tg_id, "<b>Ключ не найден в базе данных.</b>", parse_mode="HTML"
)
finally:
await conn.close()
except Exception as e:
await handle_error(
tg_id, callback_query, f"Ошибка при обновлении подписки: {e}"
)
await callback_query.answer()
@router.callback_query(F.data.startswith("delete_key|"))
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
confirmation_keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="✅ Да, удалить",
callback_data=f"confirm_delete|{client_id}",
)
],
[
types.InlineKeyboardButton(
text="❌ Нет, отменить", callback_data="view_keys"
)
],
]
)
await bot.send_message(
chat_id=tg_id,
text="<b>Вы уверены, что хотите удалить ключ?</b>",
reply_markup=confirmation_keyboard,
parse_mode="HTML",
)
except Exception as e:
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при удалении ключа:</b> {e}",
parse_mode="HTML",
)
await callback_query.answer()
@router.callback_query(F.data.startswith("renew_key|"))
async def process_callback_renew_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split("|")[1]
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1", client_id
)
if record:
expiry_time = record["expiry_time"]
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[instructions_button],
[renew_button, delete_button],
[change_location_button],
[back_button]
[
types.InlineKeyboardButton(
text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)',
callback_data=f"renew_plan|1|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)',
callback_data=f"renew_plan|3|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)',
callback_data=f"renew_plan|6|{client_id}",
)
],
[
types.InlineKeyboardButton(
text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)',
callback_data=f"renew_plan|12|{client_id}",
)
],
[
types.InlineKeyboardButton(
text="🔙 Назад", callback_data="view_profile"
)
],
]
)
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
else:
await bot.edit_message_text("<b>Информация о ключе не найдена.</b>", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML")
finally:
await conn.close()
except Exception as e:
await handle_error(tg_id, callback_query, f"Ошибка при получении информации о ключе: {e}")
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('delete_key|'))
async def process_callback_delete_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
confirmation_keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='✅ Да, удалить', callback_data=f'confirm_delete|{client_id}')],
[types.InlineKeyboardButton(text='❌ Нет, отменить', callback_data='view_keys')]
])
await bot.edit_message_text("<b>Вы уверены, что хотите удалить ключ?</b>", chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=confirmation_keyboard, parse_mode="HTML")
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('renew_key|'))
async def process_callback_renew_key(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email, expiry_time FROM keys WHERE client_id = $1', client_id)
if record:
email = record['email']
expiry_time = record['expiry_time']
current_time = datetime.utcnow().timestamp() * 1000
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text=f'📅 1 месяц ({RENEWAL_PLANS["1"]["price"]} руб.)', callback_data=f'renew_plan|1|{client_id}')],
[types.InlineKeyboardButton(text=f'📅 3 месяца ({RENEWAL_PLANS["3"]["price"]} руб.)', callback_data=f'renew_plan|3|{client_id}')],
[types.InlineKeyboardButton(text=f'📅 6 месяцев ({RENEWAL_PLANS["6"]["price"]} руб.)', callback_data=f'renew_plan|6|{client_id}')],
[types.InlineKeyboardButton(text=f'📅 12 месяцев ({RENEWAL_PLANS["12"]["price"]} руб.)', callback_data=f'renew_plan|12|{client_id}')],
[types.InlineKeyboardButton(text='🔙 Назад', callback_data='view_profile')]
])
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.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
await bot.send_message(
chat_id=tg_id,
text=response_message,
reply_markup=keyboard,
parse_mode="HTML",
)
else:
response_message = "<b>Ключ не найден.</b>"
await bot.send_message(
chat_id=tg_id, text=response_message, parse_mode="HTML"
)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(f"<b>Ошибка при выборе плана:</b> {e}", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode="HTML")
await bot.send_message(
chat_id=tg_id,
text=f"<b>Ошибка при выборе плана:</b> {e}",
parse_mode="HTML",
)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('confirm_delete|'))
@router.callback_query(F.data.startswith("confirm_delete|"))
async def process_callback_confirm_delete(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
client_id = callback_query.data.split("|")[1]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email, server_id FROM keys WHERE client_id = $1', client_id)
record = await conn.fetchrow(
"SELECT email FROM keys WHERE client_id = $1", client_id
)
if record:
email = record['email']
server_id = record['server_id']
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success = await delete_client(session, server_id, client_id)
email = record["email"]
response_message = "Ключ успешно удален."
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
if success:
await conn.execute('DELETE FROM keys WHERE client_id = $1', client_id)
response_message = "Ключ был успешно удален."
else:
response_message = "Ошибка при удалении клиента через API."
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,
)
async def delete_key_from_servers():
try:
tasks = []
for cluster_id, cluster in CLUSTERS.items():
tasks.append(
delete_key_from_cluster(cluster_id, email, client_id)
)
await asyncio.gather(*tasks)
except Exception as e:
logger.error(f"Ошибка при удалении ключа {client_id}: {e}")
asyncio.create_task(delete_key_from_servers())
await delete_key_from_db(client_id)
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(f"Ошибка при удалении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('renew_plan|'))
async def process_callback_renew_plan(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
plan, client_id = callback_query.data.split('|')[1], callback_query.data.split('|')[2]
days_to_extend = 30 * int(plan)
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow('SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1', client_id)
if record:
email = record['email']
expiry_time = record['expiry_time']
server_id = record['server_id']
current_time = datetime.utcnow().timestamp() * 1000
if expiry_time <= current_time:
new_expiry_time = int(current_time + timedelta(days=days_to_extend).total_seconds() * 1000)
else:
new_expiry_time = int(expiry_time + timedelta(days=days_to_extend).total_seconds() * 1000)
cost = RENEWAL_PLANS[plan]['price']
balance = await get_balance(tg_id)
if balance < cost:
replenish_button = types.InlineKeyboardButton(text='Пополнить баланс', callback_data='replenish_balance')
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[replenish_button], [back_button]])
await bot.edit_message_text(INSUFFICIENT_FUNDS_MSG, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
return
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time)
if success:
await update_balance(tg_id, -cost)
await conn.execute('UPDATE keys SET expiry_time = $1 WHERE client_id = $2', new_expiry_time, client_id)
response_message = SUCCESS_RENEWAL_MSG.format(months=RENEWAL_PLANS[plan]['months'])
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_profile')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.edit_message_text(response_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard)
else:
await bot.edit_message_text(ERROR_RENEWAL_MSG, chat_id=tg_id, message_id=callback_query.message.message_id)
else:
await bot.edit_message_text(KEY_NOT_FOUND_MSG, chat_id=tg_id, message_id=callback_query.message.message_id)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(f"Ошибка при продлении ключа: {e}", chat_id=tg_id, message_id=callback_query.message.message_id)
await callback_query.answer()
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)
@router.callback_query(lambda c: c.data.startswith('change_location|'))
async def process_callback_change_location(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
client_id = callback_query.data.split('|')[1]
instructions_message = (
"<b>Перед сменой локации:</b>\n\n"
"1. Пожалуйста, отключите ваш VPN.\n"
"2. Удалите старый ключ, чтобы избежать конфликтов.\n\n"
"Теперь выберите новый сервер для вашего ключа:"
)
server_buttons = []
conn = await asyncpg.connect(DATABASE_URL)
try:
for server_id, server in SERVERS.items():
count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id)
percent_full = (count / 60) * 100 if count <= 60 else 100
server_name = f"{server['name']} ({percent_full:.1f}%)"
server_buttons.append([types.InlineKeyboardButton(text=server_name, callback_data=f'select_server&{server_id}&{client_id}')])
finally:
await conn.close()
keyboard = types.InlineKeyboardMarkup(inline_keyboard=server_buttons)
await bot.edit_message_text(instructions_message, chat_id=tg_id, message_id=callback_query.message.message_id, reply_markup=keyboard, parse_mode="HTML")
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('select_server&'))
async def process_callback_select_server(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
server_id, client_id = callback_query.data.split('&')[1], callback_query.data.split('&')[2]
try:
conn = await asyncpg.connect(DATABASE_URL)
try:
async with conn.transaction():
record = await conn.fetchrow(
'SELECT email, expiry_time, server_id FROM keys WHERE client_id = $1 FOR UPDATE', client_id
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_keys"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
if record:
email = record['email']
expiry_time = record['expiry_time']
current_server_id = record['server_id']
if current_server_id == server_id:
await callback_query.answer("Клиент уже на этом сервере.")
return
session_new = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
new_client_data = await add_client(
session_new, server_id, client_id, email, tg_id, limit_ip=1, total_gb=0,
expiry_time=int(datetime.utcnow().timestamp() * 1000) + (expiry_time - datetime.utcnow().timestamp() * 1000),
enable=True, flow="xtls-rprx-vision"
)
if not new_client_data:
raise Exception("Ошибка при создании клиента на новом сервере.")
new_key = await link_subscription(email, server_id)
await conn.execute(
'UPDATE keys SET server_id = $1, key = $2 WHERE client_id = $3',
server_id, new_key, client_id
)
try:
session_old = await login_with_credentials(current_server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success_delete = await delete_client(session_old, current_server_id, client_id)
if not success_delete:
raise Exception(f"Ошибка при удалении клиента с сервера {current_server_id}")
response_message = key_relocated(new_key)
except Exception as e:
response_message = f"Ключ перемещен, но возникла ошибка при удалении клиента с текущего сервера: {e}"
else:
response_message = "Ключ не найден или уже удален."
back_button = types.InlineKeyboardButton(text='Назад', callback_data='view_keys')
iphone_button = types.InlineKeyboardButton(
text='🍏IPhone',
url=f'{APP_URL}/?url=v2raytun://import/{new_key}'
)
android_button = types.InlineKeyboardButton(
text='🤖Android',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={new_key}'
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[back_button],
[iphone_button, android_button]
]
)
await bot.edit_message_text(
response_message, chat_id=tg_id, message_id=callback_query.message.message_id,
reply_markup=keyboard, parse_mode='HTML'
)
await bot.edit_message_text(
response_message,
chat_id=tg_id,
message_id=callback_query.message.message_id,
reply_markup=keyboard,
)
finally:
await conn.close()
except Exception as e:
await bot.edit_message_text(
f"Ошибка при смене локации: {e}", chat_id=tg_id, message_id=callback_query.message.message_id, parse_mode='HTML'
f"Ошибка при удалении ключа: {e}",
chat_id=tg_id,
message_id=callback_query.message.message_id,
)
await callback_query.answer()
@router.callback_query(F.data.startswith("renew_plan|"))
async def process_callback_renew_plan(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
plan, client_id = (
callback_query.data.split("|")[1],
callback_query.data.split("|")[2],
)
days_to_extend = 30 * int(plan)
gb_multiplier = {"1": 1, "3": 3, "6": 6, "12": 12}
total_gb = TOTAL_GB * gb_multiplier.get(plan, 1) if TOTAL_GB > 0 else 0
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
conn = await asyncpg.connect(DATABASE_URL)
try:
record = await conn.fetchrow(
"SELECT email, expiry_time FROM keys WHERE client_id = $1", client_id
)
if record:
email = record["email"]
expiry_time = record["expiry_time"]
current_time = datetime.utcnow().timestamp() * 1000
if expiry_time <= current_time:
new_expiry_time = int(
current_time
+ timedelta(days=days_to_extend).total_seconds() * 1000
)
else:
new_expiry_time = int(
expiry_time
+ timedelta(days=days_to_extend).total_seconds() * 1000
)
cost = RENEWAL_PLANS[plan]["price"]
balance = await get_balance(tg_id)
if balance < cost:
replenish_button = types.InlineKeyboardButton(
text="Пополнить баланс", callback_data="pay"
)
view_profile = types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[[replenish_button], [view_profile]]
)
await bot.send_message(
tg_id,
INSUFFICIENT_FUNDS_MSG,
reply_markup=keyboard,
parse_mode="HTML",
)
return
response_message = SUCCESS_RENEWAL_MSG.format(
months=RENEWAL_PLANS[plan]["months"]
)
back_button = types.InlineKeyboardButton(
text="Назад", callback_data="view_profile"
)
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[back_button]])
await bot.send_message(
tg_id, response_message, reply_markup=keyboard, parse_mode="HTML"
)
async def renew_key_on_servers():
tasks = []
for cluster_id in CLUSTERS:
task = asyncio.create_task(
renew_key_in_cluster(
cluster_id, email, client_id, new_expiry_time, total_gb
)
)
tasks.append(task)
await asyncio.gather(*tasks)
await update_balance(tg_id, -cost)
await update_key_expiry(client_id, new_expiry_time)
await renew_key_on_servers()
else:
await bot.send_message(tg_id, KEY_NOT_FOUND_MSG, parse_mode="HTML")
finally:
await conn.close()
except Exception as e:
await bot.send_message(
tg_id, f"Ошибка при продлении ключа: {e}", parse_mode="HTML"
)
await callback_query.answer()
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+75
View File
@@ -0,0 +1,75 @@
import base64
import aiohttp
from aiohttp import web
from config import CLUSTERS
from logger import logger
async def fetch_url_content(url):
try:
logger.debug(f"Получение URL: {url}")
async with aiohttp.ClientSession() as session:
async with session.get(url, ssl=False) as response:
if response.status == 200:
content = await response.text()
logger.debug(f"Успешно получен контент с {url}")
return base64.b64decode(content).decode("utf-8").split("\n")
else:
logger.error(
f"Не удалось получить {url}, статус: {response.status}"
)
return []
except Exception as e:
logger.error(f"Ошибка при получении {url}: {e}")
return []
async def combine_unique_lines(urls, query_string):
all_lines = []
logger.debug(f"Начинаем объединение подписок для запроса: {query_string}")
urls_with_query = [f"{url}?{query_string}" for url in urls]
logger.debug(f"Составлены URL-адреса: {urls_with_query}")
for url in urls_with_query:
lines = await fetch_url_content(url)
all_lines.extend(lines)
all_lines = list(set(filter(None, all_lines)))
logger.debug(
f"Объединено {len(all_lines)} строк после фильтрации и удаления дубликатов"
)
return all_lines
async def handle_subscription(request):
email = request.match_info["email"]
logger.info(f"Получен запрос на подписку для email: {email}")
urls = []
for cluster in CLUSTERS.values():
for server in cluster.values():
server_subscription_url = f"{server['SUBSCRIPTION']}/{email}"
urls.append(server_subscription_url)
query_string = request.query_string
logger.debug(f"Извлечен query string: {query_string}")
combined_subscriptions = await combine_unique_lines(urls, query_string)
base64_encoded = base64.b64encode(
"\n".join(combined_subscriptions).encode("utf-8")
).decode("utf-8")
headers = {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
"profile-update-interval": "7",
"profile-title": email,
}
logger.info(f"Возвращаем объединенные подписки для email: {email}")
return web.Response(text=base64_encoded, headers=headers)
+97 -37
View File
@@ -1,51 +1,111 @@
import asyncpg
import asyncio
import uuid
from config import DATABASE_URL, SERVERS, ADMIN_USERNAME, ADMIN_PASSWORD
from auth import login_with_credentials, link_subscription
from client import add_client
from database import store_key, add_connection
from handlers.texts import INSTRUCTIONS
from datetime import datetime, timedelta
from handlers.utils import generate_random_email, get_least_loaded_server
import asyncpg
from py3xui import AsyncApi
from client import add_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, PUBLIC_LINK, TRIAL_TIME
from database import store_key
from handlers.texts import INSTRUCTIONS
from handlers.utils import generate_random_email, get_least_loaded_cluster
async def create_trial_key(tg_id: int):
conn = await asyncpg.connect(DATABASE_URL)
try:
server_id = await get_least_loaded_server(conn)
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
current_time = datetime.utcnow()
expiry_time = current_time + timedelta(days=1, hours=3)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
client_id = str(uuid.uuid4())
email = generate_random_email()
response = await add_client(
session, server_id, client_id, email, tg_id,
limit_ip=1, total_gb=0, expiry_time=expiry_timestamp,
enable=True, flow="xtls-rprx-vision"
public_link = f"{PUBLIC_LINK}{email}"
instructions = INSTRUCTIONS
result = {"key": public_link, "instructions": instructions}
asyncio.create_task(
generate_and_store_keys(tg_id, client_id, email, public_link)
)
if response.get("success"):
# Генерация ссылки подписки
connection_link = await link_subscription(email, server_id)
return result
existing_connection = await conn.fetchrow('SELECT * FROM connections WHERE tg_id = $1', tg_id)
if existing_connection:
await conn.execute('UPDATE connections SET trial = 1 WHERE tg_id = $1', tg_id)
else:
await add_connection(tg_id, 0, 1)
await store_key(tg_id, client_id, email, expiry_timestamp, connection_link, server_id)
instructions = INSTRUCTIONS
return {
'key': connection_link,
'instructions': instructions
}
else:
return {'error': 'Не удалось добавить клиента на панель'}
finally:
await conn.close()
async def generate_and_store_keys(
tg_id: int, client_id: str, email: str, public_link: str
):
conn = await asyncpg.connect(DATABASE_URL)
try:
current_time = datetime.utcnow()
expiry_time = current_time + timedelta(days=TRIAL_TIME, hours=3)
expiry_timestamp = int(expiry_time.timestamp() * 1000)
least_loaded_cluster = await get_least_loaded_cluster()
tasks = []
for server_id, server in CLUSTERS[least_loaded_cluster].items():
task = create_key_on_server(
least_loaded_cluster,
server_id,
client_id,
email,
tg_id,
expiry_timestamp,
)
tasks.append(task)
await asyncio.gather(*tasks)
await store_key(
tg_id,
client_id,
email,
expiry_timestamp,
public_link,
server_id=least_loaded_cluster,
)
await conn.execute(
"""
INSERT INTO connections (tg_id, trial)
VALUES ($1, 1)
ON CONFLICT (tg_id)
DO UPDATE SET trial = 1
""",
tg_id,
)
finally:
await conn.close()
async def create_key_on_server(
cluster_id: str,
server_id: str,
client_id: str,
email: str,
tg_id: int,
expiry_timestamp: int,
):
"""Создает ключ на сервере в указанном кластере и возвращает результат."""
xui = AsyncApi(
CLUSTERS[cluster_id][server_id]["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
response = await add_client(
xui,
client_id,
email,
tg_id,
limit_ip=1,
total_gb=0,
expiry_time=expiry_timestamp,
enable=True,
flow="xtls-rprx-vision",
)
return response
+214 -119
View File
@@ -1,41 +1,40 @@
from datetime import datetime, timedelta
import asyncpg
import asyncio
from aiogram import Bot, Router
from aiogram.fsm.state import State, StatesGroup
import logging
from config import DATABASE_URL, ADMIN_USERNAME, ADMIN_PASSWORD, SERVERS
from database import get_balance, update_key_expiry, delete_key, update_balance
from client import extend_client_key, delete_client
from auth import login_with_credentials
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, KEY_RENEWAL_FAILED
from aiogram import types
from datetime import datetime, timedelta
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
import asyncpg
from aiogram import Bot, Router, types
from py3xui import AsyncApi
from client import delete_client
from config import ADMIN_PASSWORD, ADMIN_USERNAME, CLUSTERS, DATABASE_URL, TOTAL_GB
from database import delete_key, get_balance, update_balance, update_key_expiry
from handlers.keys.key_utils import renew_key_in_cluster
from handlers.texts import KEY_EXPIRY_10H, KEY_EXPIRY_24H, KEY_RENEWED, RENEWAL_PLANS
from logger import logger
router = Router()
class NotificationStates(StatesGroup):
waiting_for_notification_text = State()
async def notify_expiring_keys(bot: Bot):
conn = None
try:
conn = await asyncpg.connect(DATABASE_URL)
logger.info("Подключение к базе данных успешно.")
current_time = datetime.utcnow().timestamp() * 1000
threshold_time_10h = (datetime.utcnow() + timedelta(hours=10)).timestamp() * 1000
threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
threshold_time_10h = (
datetime.utcnow() + timedelta(hours=10)
).timestamp() * 1000
threshold_time_24h = (datetime.utcnow() + timedelta(days=1)).timestamp() * 1000
logger.info("Начало обработки уведомлений.")
await notify_10h_keys(bot, conn, current_time, threshold_time_10h)
await asyncio.sleep(1)
await asyncio.sleep(1)
await notify_24h_keys(bot, conn, current_time, threshold_time_24h)
await asyncio.sleep(1)
await asyncio.sleep(1)
await handle_expired_keys(bot, conn, current_time)
await asyncio.sleep(1)
except Exception as e:
logger.error(f"Ошибка при отправке уведомлений: {e}")
@@ -44,26 +43,39 @@ async def notify_expiring_keys(bot: Bot):
await conn.close()
logger.info("Соединение с базой данных закрыто.")
async def is_bot_blocked(bot: Bot, chat_id: int) -> bool:
try:
member = await bot.get_chat_member(chat_id, bot.id)
return member.status == 'left'
blocked = member.status == "left"
logger.info(
f"Статус бота для пользователя {chat_id}: {'заблокирован' if blocked else 'активен'}"
)
return blocked
except Exception as e:
logger.error(f"Ошибка при проверке статуса бота у пользователя {chat_id}: {e}")
return False
logger.warning(
f"Не удалось проверить статус бота для пользователя {chat_id}: {e}"
)
return False
async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float):
records = await conn.fetch('''
async def notify_10h_keys(
bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_10h: float
):
records = await conn.fetch(
"""
SELECT tg_id, email, expiry_time, client_id, server_id FROM keys
WHERE expiry_time <= $1 AND expiry_time > $2 AND notified = FALSE
''', threshold_time_10h, current_time)
""",
threshold_time_10h,
current_time,
)
logger.info(f"Найдено {len(records)} ключей для уведомления за 10 часов.")
for record in records:
tg_id = record['tg_id']
email = record['email']
expiry_time = record['expiry_time']
server_id = record['server_id']
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
@@ -77,46 +89,71 @@ async def notify_10h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
hours_left = time_left.seconds // 3600
days_left_message = f"{hours_left}"
server_name = SERVERS[server_id]['name']
message = KEY_EXPIRY_10H.format(
server_id=server_name,
email=email,
expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S'),
days_left_message=days_left_message
email=email,
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
days_left_message=days_left_message,
)
if not await is_bot_blocked(bot, tg_id):
try:
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='🔄 Продлить VPN', callback_data=f'renew_key|{record["client_id"]}')],
[types.InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')],
[types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')]
])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
],
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
],
]
)
await bot.send_message(tg_id, message, reply_markup=keyboard)
logger.info(f"Уведомление отправлено пользователю {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {tg_id}: {e}")
continue
logger.error(
f"Ошибка при отправке уведомления пользователю {tg_id}: {e}"
)
continue
await conn.execute('UPDATE keys SET notified = TRUE WHERE client_id = $1', record['client_id'])
await conn.execute(
"UPDATE keys SET notified = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(f"Обновлено поле notified для клиента {record['client_id']}.")
await asyncio.sleep(1)
async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float):
await asyncio.sleep(1)
async def notify_24h_keys(
bot: Bot, conn: asyncpg.Connection, current_time: float, threshold_time_24h: float
):
logger.info("Проверка истекших ключей...")
records_24h = await conn.fetch('''
records_24h = await conn.fetch(
"""
SELECT tg_id, email, expiry_time, client_id, server_id FROM keys
WHERE expiry_time <= $1 AND expiry_time > $2 AND notified_24h = FALSE
''', threshold_time_24h, current_time)
""",
threshold_time_24h,
current_time,
)
logger.info(f"Найдено {len(records_24h)} ключей для уведомления за 24 часа.")
for record in records_24h:
tg_id = record['tg_id']
email = record['email']
expiry_time = record['expiry_time']
server_id = record['server_id']
tg_id = record["tg_id"]
email = record["email"]
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
@@ -131,102 +168,160 @@ async def notify_24h_keys(bot: Bot, conn: asyncpg.Connection, current_time: floa
days_left_message = f"{hours_left}"
message_24h = KEY_EXPIRY_24H.format(
server_id=SERVERS[server_id]['name'],
email=email,
days_left_message=days_left_message,
expiry_date=expiry_date.strftime('%Y-%m-%d %H:%M:%S')
expiry_date=expiry_date.strftime("%Y-%m-%d %H:%M:%S"),
)
if not await is_bot_blocked(bot, tg_id):
try:
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[
[types.InlineKeyboardButton(text='🔄 Продлить VPN', callback_data=f'renew_key|{record["client_id"]}')],
[types.InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='replenish_balance')],
[types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')]
])
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="🔄 Продлить VPN",
callback_data=f'renew_key|{record["client_id"]}',
)
],
[
types.InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
],
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
],
]
)
await bot.send_message(tg_id, message_24h, reply_markup=keyboard)
logger.info(f"Уведомление за 24 часа отправлено пользователю {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}")
continue
logger.error(
f"Ошибка при отправке уведомления за 24 часа пользователю {tg_id}: {e}"
)
continue
await conn.execute('UPDATE keys SET notified_24h = TRUE WHERE client_id = $1', record['client_id'])
logger.info(f"Обновлено поле notified_24h для клиента {record['client_id']}.")
await asyncio.sleep(1)
await conn.execute(
"UPDATE keys SET notified_24h = TRUE WHERE client_id = $1",
record["client_id"],
)
logger.info(
f"Обновлено поле notified_24h для клиента {record['client_id']}."
)
await asyncio.sleep(1)
async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time: float):
logger.info("Проверка истекших ключей...")
current_time = datetime.utcnow().timestamp() * 1000
adjusted_current_time = current_time + (3 * 60 * 60 * 1000)
logger.info(f"Текущее время: {current_time}, Скорректированное текущее время: {adjusted_current_time}")
expiring_keys = await conn.fetch('''
SELECT tg_id, client_id, expiry_time, server_id, email FROM keys
expiring_keys = await conn.fetch(
"""
SELECT tg_id, client_id, expiry_time, email FROM keys
WHERE expiry_time <= $1
''', adjusted_current_time)
""",
adjusted_current_time,
)
logger.info(f"Найдено {len(expiring_keys)} истекающих ключей.")
for record in expiring_keys:
tg_id = record['tg_id']
client_id = record['client_id']
async def process_key(record):
tg_id = record["tg_id"]
client_id = record["client_id"]
email = record["email"]
balance = await get_balance(tg_id)
server_id = record['server_id']
email = record['email']
logger.info(f"Проверка баланса для клиента {tg_id}: {balance}.")
expiry_time = record['expiry_time']
expiry_time = record["expiry_time"]
expiry_date = datetime.utcfromtimestamp(expiry_time / 1000)
current_date = datetime.utcnow()
time_left = expiry_date - current_date
logger.info(f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}.")
if time_left.total_seconds() <= 0:
days_left_message = "Ключ истек"
elif time_left.days > 0:
days_left_message = f"Осталось дней: <b>{time_left.days}</b>"
else:
hours_left = time_left.seconds // 3600
days_left_message = f"Осталось часов: <b>{hours_left}</b>"
logger.info(
f"Время истечения ключа: {expiry_time} (дата: {expiry_date}), Текущее время: {current_date}, Оставшееся время: {time_left}"
)
message_expired = f"Ваш ключ {email} для сервера {SERVERS[server_id]['name']} истек и был удален!\n\n Перейдите в профиль для создания нового ключа"
message_expired = (
f"❌ Ваша подписка {email} истекла и была удалена!\n\n"
"🔍 Перейдите в профиль для создания новой подписки.\n"
"💡 Не откладывайте подключение VPN!"
)
keyboard = types.InlineKeyboardMarkup(
inline_keyboard=[
[
types.InlineKeyboardButton(
text="👤 Личный кабинет", callback_data="view_profile"
)
]
]
)
button_profile = types.InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
keyboard = types.InlineKeyboardMarkup(inline_keyboard=[[button_profile]])
try:
if balance >= RENEWAL_PLANS["1"]["price"]:
await update_balance(tg_id, -RENEWAL_PLANS["1"]["price"])
new_expiry_time = int(
(datetime.utcnow() + timedelta(days=30)).timestamp() * 1000
)
await update_key_expiry(client_id, new_expiry_time)
if balance >= 100:
await update_balance(tg_id, -100)
new_expiry_time = int((datetime.utcnow() + timedelta(days=30)).timestamp() * 1000)
await update_key_expiry(client_id, new_expiry_time)
logger.info(f"Ключ для клиента {tg_id} продлен до {datetime.utcfromtimestamp(new_expiry_time / 1000).strftime('%Y-%m-%d %H:%M:%S')}.")
for cluster_id in CLUSTERS:
await renew_key_in_cluster(
cluster_id, email, client_id, new_expiry_time, TOTAL_GB
)
logger.info(
f"Ключ для пользователя {tg_id} успешно продлен в кластере {cluster_id}."
)
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success = await extend_client_key(session, server_id, tg_id, client_id, email, new_expiry_time)
if success:
await conn.execute(
"""
UPDATE keys
SET notified = FALSE, notified_24h = FALSE
WHERE client_id = $1
""",
client_id,
)
logger.info(
f"Флаги notified и notified_24 сброшены для клиента с ID {client_id}."
)
try:
await bot.send_message(tg_id, KEY_RENEWED, reply_markup=keyboard)
logger.info(f"Ключ для пользователя {tg_id} успешно продлен на месяц.")
await bot.send_message(
tg_id, text=KEY_RENEWED, reply_markup=keyboard
)
logger.info(
f"Уведомление об успешном продлении отправлено клиенту {tg_id}."
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления о продлении ключа пользователю {tg_id}: {e}")
logger.error(
f"Ошибка при отправке уведомления клиенту {tg_id}: {e}"
)
else:
try:
await bot.send_message(tg_id, KEY_RENEWAL_FAILED, reply_markup=keyboard)
logger.error(f"Не удалось продлить ключ для пользователя {tg_id}.")
except Exception as e:
logger.error(f"Ошибка при отправке уведомления о неудачном продлении ключа пользователю {tg_id}: {e}")
else:
try:
await bot.send_message(tg_id, message_expired, reply_markup=keyboard)
await safe_send_message(
bot, tg_id, message_expired, reply_markup=keyboard
)
await delete_key(client_id)
session = await login_with_credentials(server_id, ADMIN_USERNAME, ADMIN_PASSWORD)
success = await delete_client(session, server_id, client_id)
logger.info(f"Ключ для клиента {tg_id} удален из базы данных.")
except Exception as e:
logger.error(f"Ошибка при удалении ключа для клиента {tg_id}: {e}")
await asyncio.sleep(1)
for cluster_id, cluster in CLUSTERS.items():
for server_id, server in cluster.items():
xui = AsyncApi(
server["API_URL"],
username=ADMIN_USERNAME,
password=ADMIN_PASSWORD,
)
await delete_client(xui, email, client_id)
except Exception as e:
logger.error(f"Ошибка при обработке ключа для клиента {tg_id}: {e}")
await asyncio.gather(*[process_key(record) for record in expiring_keys])
async def safe_send_message(bot, tg_id, text, reply_markup=None):
try:
await bot.send_message(tg_id, text, reply_markup=reply_markup)
except Exception as e:
if "chat not found" in str(e):
logger.warning(f"Чат для клиента {tg_id} не найден.")
else:
logger.error(f"Ошибка при отправке сообщения клиенту {tg_id}: {e}")
+79
View File
@@ -0,0 +1,79 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import CRYPTO_BOT_ENABLE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, STARS_ENABLE, YOOKASSA_ENABLE
from database import get_trial
from handlers.start import send_welcome_message
router = Router()
@router.callback_query(F.data == "pay")
async def handle_pay(callback_query: CallbackQuery):
await callback_query.message.delete()
builder = InlineKeyboardBuilder()
if YOOKASSA_ENABLE:
builder.row(
InlineKeyboardButton(
text="💳 ЮКасса: быстрый перевод",
callback_data="pay_yookassa",
)
)
if FREEKASSA_ENABLE:
builder.row(
InlineKeyboardButton(
text="🌐 FreeKassa: множество способов",
callback_data="pay_freekassa",
)
)
if CRYPTO_BOT_ENABLE:
builder.row(
InlineKeyboardButton(
text="💰 CryptoBot: криптовалюта",
callback_data="pay_cryptobot",
)
)
if STARS_ENABLE:
builder.row(
InlineKeyboardButton(
text="⭐ Оплата Звездами",
callback_data="pay_stars",
)
)
if ROBOKASSA_ENABLE:
builder.row(
InlineKeyboardButton(
text="⭐ RoboKassa",
callback_data="pay_robokassa",
)
)
builder.row(
InlineKeyboardButton(
text="🎟️ Активировать купон", callback_data="activate_coupon"
)
)
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
await callback_query.message.answer(
"💸 <b>Выберите удобный способ пополнения баланса:</b>\n\n"
"• Быстро и безопасно\n"
"• Поддержка разных платежных систем\n"
"• Моментальное зачисление средств 🚀",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
await send_welcome_message(callback_query.from_user.id, trial_status)
await callback_query.answer()
-159
View File
@@ -1,159 +0,0 @@
import uuid
import hashlib
import requests
import logging
import time
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 bot import bot
from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID
from database import update_balance
router = Router()
logging.basicConfig(level=logging.DEBUG)
class ReplenishBalanceState(StatesGroup):
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
def generate_signature(params, api_key):
sign_string = ":".join([str(params[k]) for k in sorted(params)]) + api_key
return hashlib.md5(sign_string.encode()).hexdigest()
async def create_payment(user_id, amount, email, ip):
payment_id = str(uuid.uuid4())
nonce = int(time.time() * 1000)
params = {
"shopId": FREEKASSA_SHOP_ID,
"amount": amount,
"currency": "RUB",
"paymentId": payment_id,
"email": email,
"ip": ip,
"i": 6,
"nonce": nonce
}
params["signature"] = generate_signature(params, FREEKASSA_API_KEY)
try:
response = requests.post("https://api.freekassa.com/v1/orders/create", json=params)
response_data = response.json()
logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}")
if response_data.get("type") == "success":
return response_data["location"]
else:
logging.error(f"Ошибка создания платежа: {response_data}")
return None
except Exception as e:
logging.error(f"Ошибка запроса к FreeKassa: {e}")
return None
async def send_payment_success_notification(user_id, amount):
try:
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!"
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def freekassa_webhook(request):
data = await request.json()
logging.debug(f"Получен вебхук от FreeKassa: {data}")
logging.debug(f"Данные вебхука от FreeKassa: {data}")
if data["status"] == "completed":
user_id = data["metadata"]["user_id"]
amount = float(data["amount"])
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
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):
tg_id = callback_query.from_user.id
amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="100 рублей", callback_data="amount|100"),
InlineKeyboardButton(text="500 рублей", callback_data="amount|500")
],
[
InlineKeyboardButton(text="1000 рублей", callback_data="amount|1000"),
InlineKeyboardButton(text="5000 рублей", callback_data="amount|5000")
],
[
InlineKeyboardButton(text="Введите другую сумму", callback_data="enter_custom_amount")
]
])
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения через FreeKassa:",
reply_markup=amount_keyboard
)
await state.set_state(ReplenishBalanceState.choosing_amount)
await callback_query.answer()
@router.callback_query(lambda c: c.data.startswith('amount|'))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
data = callback_query.data.split('|', 1)
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await bot.send_message(callback_query.from_user.id, "Некорректная сумма.")
return
user_email = f"{callback_query.from_user.id}@solo.net"
user_ip = callback_query.message.chat.id
payment_url = await create_payment(callback_query.from_user.id, amount, user_email, user_ip)
if payment_url:
await bot.send_message(callback_query.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}")
else:
await bot.send_message(callback_query.from_user.id, "Ошибка при создании платежа. Попробуйте позже.")
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'enter_custom_amount')
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(
text="Введите сумму пополнения:"
)
await state.set_state(ReplenishBalanceState.entering_custom_amount)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:")
return
user_email = f"{message.from_user.id}@solo.net"
user_ip = message.chat.id
payment_url = await create_payment(message.from_user.id, amount, user_email, user_ip)
if payment_url:
await bot.send_message(message.from_user.id, f"Перейдите по ссылке для оплаты: {payment_url}")
else:
await message.answer("Ошибка при создании платежа. Попробуйте позже.")
else:
await message.answer("Пожалуйста, введите корректную сумму.")
-280
View File
@@ -1,280 +0,0 @@
import logging
import uuid
from aiogram import Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiohttp import web
from yookassa import Configuration, Payment
from bot import bot
from config import YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID
from database import (add_connection, check_connection_exists, get_key_count,
update_balance)
from handlers.profile import process_callback_view_profile
from handlers.texts import PAYMENT_OPTIONS
router = Router()
logging.basicConfig(level=logging.DEBUG)
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
logging.debug(f"Account ID: {YOOKASSA_SHOP_ID}")
logging.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}")
class ReplenishBalanceState(StatesGroup):
choosing_amount = State()
waiting_for_payment_confirmation = State()
entering_custom_amount = State()
async def send_message_with_deletion(chat_id, text, reply_markup=None, state=None, message_key='last_message_id'):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(chat_id=chat_id, message_id=previous_message_id)
sent_message = await bot.send_message(chat_id=chat_id, text=text, reply_markup=reply_markup)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
print(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(lambda c: c.data == 'replenish_balance')
async def process_callback_replenish_balance(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
key_count = await get_key_count(tg_id)
if key_count == 0:
exists = await check_connection_exists(tg_id)
if not exists:
await add_connection(tg_id, balance=0.0, trial=0)
amount_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[0]['text'], callback_data=PAYMENT_OPTIONS[0]['callback_data']),
InlineKeyboardButton(text=PAYMENT_OPTIONS[1]['text'], callback_data=PAYMENT_OPTIONS[1]['callback_data'])
],
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[2]['text'], callback_data=PAYMENT_OPTIONS[2]['callback_data']),
InlineKeyboardButton(text=PAYMENT_OPTIONS[3]['text'], callback_data=PAYMENT_OPTIONS[3]['callback_data'])
],
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[4]['text'], callback_data=PAYMENT_OPTIONS[4]['callback_data'])
],
[
InlineKeyboardButton(text=PAYMENT_OPTIONS[5]['text'], callback_data=PAYMENT_OPTIONS[5]['callback_data'])
],
])
await bot.delete_message(chat_id=tg_id, message_id=callback_query.message.message_id)
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=amount_keyboard
)
await state.set_state(ReplenishBalanceState.choosing_amount)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'back_to_profile')
async def back_to_profile_handler(callback_query: types.CallbackQuery, state: FSMContext):
await process_callback_view_profile(callback_query, state)
@router.callback_query(lambda c: c.data.startswith('amount_'))
async def process_amount_selection(callback_query: types.CallbackQuery, state: FSMContext):
data = callback_query.data.split('_', 1)
if len(data) != 2:
await send_message_with_deletion(callback_query.from_user.id, "Неверные данные для выбора суммы.", state=state, message_key='amount_error_message_id')
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await send_message_with_deletion(callback_query.from_user.id, "Некорректная сумма.", state=state, message_key='amount_error_message_id')
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
state_data = await state.get_data()
customer_name = callback_query.from_user.full_name
customer_id = callback_query.from_user.id
customer_email = f"{customer_id}@solo.net"
payment = Payment.create({
"amount": {
"value": str(amount),
"currency": "RUB"
},
"confirmation": {
"type": "redirect",
"return_url": "https://pocomacho.ru/"
},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": customer_name,
"email": customer_email,
"phone": "79000000000"
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {
"value": str(amount),
"currency": "RUB"
},
"vat_code": 6
}
]
},
"metadata": {
"user_id": customer_id
}
}, uuid.uuid4())
if payment['status'] == 'pending':
payment_url = payment['confirmation']['confirmation_url']
confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Пополнить', url=payment_url)],
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')]
])
await callback_query.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard
)
else:
await send_message_with_deletion(callback_query.from_user.id, "Ошибка при создании платежа.", state=state)
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
profile_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Перейти в профиль', callback_data='view_profile')]
])
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=profile_keyboard
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def payment_webhook(request):
event = await request.json()
logging.debug(f"Webhook event received: {event}")
if event['event'] == 'payment.succeeded':
user_id_str = event['object']['metadata']['user_id']
amount_str = event['object']['amount']['value']
try:
user_id = int(user_id_str)
amount = float(amount_str)
logging.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logging.error(f"Ошибка конвертации user_id или amount: {e}")
return web.Response(status=400)
return web.Response(status=200)
@router.callback_query(lambda c: c.data == 'enter_custom_amount')
async def process_enter_custom_amount(callback_query: types.CallbackQuery, state: FSMContext):
await callback_query.message.edit_text(
text="Введите сумму пополнения:"
)
await state.set_state(ReplenishBalanceState.entering_custom_amount)
await callback_query.answer()
@router.message(State(ReplenishBalanceState.entering_custom_amount))
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount <= 0:
await message.answer("Сумма должна быть больше нуля. Пожалуйста, введите сумму еще раз:")
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation)
try:
payment = Payment.create({
"amount": {
"value": str(amount),
"currency": "RUB"
},
"confirmation": {
"type": "redirect",
"return_url": "https://pocomacho.ru/"
},
"capture": True,
"description": "Пополнение баланса",
"receipt": {
"customer": {
"full_name": message.from_user.full_name,
"email": f"{message.from_user.id}@solo.net",
"phone": "79000000000"
},
"items": [
{
"description": "Пополнение баланса",
"quantity": "1.00",
"amount": {
"value": str(amount),
"currency": "RUB"
},
"vat_code": 6
}
]
},
"metadata": {
"user_id": message.from_user.id
}
}, uuid.uuid4())
if payment['status'] == 'pending':
payment_url = payment['confirmation']['confirmation_url']
confirm_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='Пополнить', url=payment_url)],
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_profile')]
])
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard
)
else:
await message.answer("Ошибка при создании платежа.")
except Exception as e:
logging.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
+272
View File
@@ -0,0 +1,272 @@
from aiocryptopay import AioCryptoPay, Networks
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from bot import bot
from config import CRYPTO_BOT_ENABLE, CRYPTO_BOT_TOKEN, RUB_TO_USDT
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
if CRYPTO_BOT_ENABLE:
crypto = AioCryptoPay(token=CRYPTO_BOT_TOKEN, network=Networks.MAIN_NET)
class ReplenishBalanceState(StatesGroup):
choosing_amount_crypto = State()
waiting_for_payment_confirmation_crypto = State()
entering_custom_amount_crypto = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_cryptobot")
async def process_callback_pay_cryptobot(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'crypto_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'crypto_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'crypto_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_crypto"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
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)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_crypto)
await callback_query.answer()
@router.callback_query(F.data.startswith("crypto_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Неверные данные для выбора суммы.")
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_crypto)
try:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(int(amount // RUB_TO_USDT)),
description=f"Пополнения баланса на {amount} руб",
payload=f"{callback_query.from_user.id}:{int(amount)}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
await bot.send_message(
chat_id=callback_query.from_user.id,
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=builder.as_markup(),
)
else:
await callback_query.message.answer("Ошибка при создании платежа.")
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def cryptobot_webhook(request):
try:
data = await request.json()
logger.info(f"Получены данные вебхука: {data}")
if data.get("update_type") == "invoice_paid":
await process_crypto_payment(data["payload"])
return web.Response(status=200)
else:
logger.warning(
f"Неподдерживаемый тип обновления: {data.get('update_type')}"
)
return web.Response(status=400)
except Exception as e:
logger.error(f"Ошибка обработки вебхука: {e}")
return web.Response(status=500)
async def process_crypto_payment(payload):
if payload["status"] == "paid":
custom_payload = payload["payload"]
user_id_str, amount_str = custom_payload.split(":")
try:
user_id = int(user_id_str)
amount = int(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
else:
logger.warning(f"Получен неоплаченный инвойс: {payload}")
@router.callback_query(F.data == "enter_custom_amount_crypto")
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_crypto)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_crypto)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_USDT <= 0:
await message.answer(
f"Сумма должна быть больше {RUB_TO_USDT}. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_crypto
)
try:
invoice = await crypto.create_invoice(
asset="USDT",
amount=str(int(amount // RUB_TO_USDT)),
description=f"Пополнения баланса на {amount} руб",
payload=f"{message.from_user.id}:{amount}",
)
if hasattr(invoice, "bot_invoice_url"):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", url=invoice.bot_invoice_url)
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await message.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=builder.as_markup(),
)
else:
await send_message_with_deletion(
message.from_user.id, "Ошибка при создании платежа.", state=state
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
+224
View File
@@ -0,0 +1,224 @@
import hashlib
import hmac
import logging
import time
import uuid
import requests
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from bot import bot
from config import FREEKASSA_API_KEY, FREEKASSA_SHOP_ID
from database import update_balance
from handlers.texts import PAYMENT_OPTIONS
router = Router()
logging.basicConfig(level=logging.DEBUG)
class ReplenishBalanceState(StatesGroup):
choosing_amount_freekassa = State()
waiting_for_payment_confirmation_freekassa = State()
entering_custom_amount_freekassa = State()
def generate_signature(params, api_key):
sorted_params = {k: params[k] for k in sorted(params)}
sign_string = "|".join(str(value) for value in sorted_params.values())
return hmac.new(api_key.encode(), sign_string.encode(), hashlib.sha256).hexdigest()
async def create_payment(user_id, amount, email, ip):
payment_id = str(uuid.uuid4())
nonce = int(time.time() * 1000)
params = {
"shopId": FREEKASSA_SHOP_ID,
"amount": amount,
"currency": "RUB",
"paymentId": payment_id,
"email": email,
"ip": ip,
"i": 6,
"nonce": nonce,
}
params["signature"] = generate_signature(params, FREEKASSA_API_KEY)
try:
response = requests.post(
"https://api.freekassa.com/v1/orders/create", json=params
)
response_data = response.json()
logging.debug(f"Ответ от FreeKassa при создании платежа: {response_data}")
if response_data.get("type") == "success":
return response_data["location"]
else:
logging.error(f"Ошибка создания платежа: {response_data}")
return None
except Exception as e:
logging.error(f"Ошибка запроса к FreeKassa: {e}")
return None
async def send_payment_success_notification(user_id, amount):
try:
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
)
except Exception as e:
logging.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def freekassa_webhook(request):
data = await request.json()
logging.debug(f"Получен вебхук от FreeKassa: {data}")
logging.debug(f"Данные вебхука от FreeKassa: {data}")
if data["status"] == "completed":
user_id = data["metadata"]["user_id"]
amount = float(data["amount"])
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
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
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'freekassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'freekassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'freekassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_freekassa"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения через FreeKassa:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_freekassa)
await callback_query.answer()
@router.callback_query(F.data.startswith("freekassa_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
await bot.send_message(callback_query.from_user.id, "Некорректная сумма.")
return
user_email = f"{callback_query.from_user.id}@solo.net"
user_ip = callback_query.message.chat.id
payment_url = await create_payment(
callback_query.from_user.id, amount, user_email, user_ip
)
if payment_url:
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text=f"Оплатить {amount} рублей", url=payment_url
)
],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay")],
]
)
await bot.send_message(
callback_query.from_user.id,
f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:",
reply_markup=confirm_keyboard,
)
else:
await bot.send_message(
callback_query.from_user.id,
"Ошибка при создании платежа. Попробуйте позже.",
)
await callback_query.answer()
@router.callback_query(F.data == "enter_custom_amount_freekassa")
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_freekassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_freekassa)
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
user_email = f"{message.from_user.id}@solo.net"
user_ip = message.chat.id
payment_url = await create_payment(
message.from_user.id, amount, user_email, user_ip
)
if payment_url:
keyboard = InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton("Оплатить", url=payment_url)]]
)
await message.answer(
f"Вы выбрали оплату на {amount} рублей. Перейдите по ссылке для завершения оплаты:",
reply_markup=keyboard,
)
else:
await message.answer("Ошибка при создании платежа. Попробуйте позже.")
else:
await message.answer("Пожалуйста, введите корректную сумму.")
+311
View File
@@ -0,0 +1,311 @@
import hashlib
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from loguru import logger
from robokassa import HashAlgorithm, Robokassa
from bot import bot
from config import ROBOKASSA_LOGIN, ROBOKASSA_PASSWORD1, ROBOKASSA_PASSWORD2, ROBOKASSA_TEST_MODE
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
router = Router()
class ReplenishBalanceState(StatesGroup):
choosing_amount_robokassa = State()
waiting_for_payment_confirmation_robokassa = State()
robokassa = Robokassa(
merchant_login=ROBOKASSA_LOGIN,
password1=ROBOKASSA_PASSWORD1,
password2=ROBOKASSA_PASSWORD2,
algorithm=HashAlgorithm.md5,
is_test=ROBOKASSA_TEST_MODE,
)
logger.info("Robokassa initialized with login: {}", ROBOKASSA_LOGIN)
def generate_payment_link(amount, inv_id, description):
"""Генерация ссылки на оплату."""
logger.debug(
f"Generating payment link for amount: {amount}, inv_id: {inv_id}, description: {description}"
)
payment_link = robokassa._payment.link.generate_by_script(
out_sum=amount,
inv_id=inv_id,
description="description",
)
logger.info(f"Generated payment link: {payment_link}")
return payment_link
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:
logger.debug(
f"Deleting previous message with ID: {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})
logger.debug(f"Sent new message with ID: {sent_message.message_id}")
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_robokassa")
async def process_callback_pay_robokassa(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
logger.info(f"User {tg_id} initiated Robokassa payment.")
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'robokassa_amount|{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_robokassa"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_profile"))
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)
logger.info(f"Created new connection for user {tg_id} with balance 0.0.")
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
logger.debug(f"Deleted message with ID: {callback_query.message.message_id}")
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_robokassa)
logger.info(f"Displayed amount selection for user {tg_id}.")
await callback_query.answer()
@router.callback_query(F.data.startswith("robokassa_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
logger.info(f"Получены данные callback_data: {callback_query.data}")
data = callback_query.data.split("|")
if len(data) != 3 or data[1] != "amount":
logger.error("Ошибка: callback_data не соответствует формату.")
await send_message_with_deletion(
chat_id=callback_query.from_user.id,
text="Неверные данные для выбора суммы.",
state=state,
)
await callback_query.answer("Ошибка: данные повреждены.")
return
amount_str = data[2]
try:
amount = int(amount_str)
if amount <= 0:
raise ValueError("Сумма должна быть положительным числом.")
except ValueError as e:
logger.error(f"Некорректное значение суммы: {amount_str}. Ошибка: {e}")
await send_message_with_deletion(
chat_id=callback_query.from_user.id,
text="Некорректная сумма. Попробуйте снова.",
state=state,
)
await callback_query.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
logger.info(f"User {callback_query.from_user.id} selected amount: {amount}.")
tg_id = callback_query.from_user.id
payment_url = generate_payment_link(
amount,
tg_id,
"Пополнение баланса",
)
logger.info(f"Payment URL for user {callback_query.from_user.id}: {payment_url}")
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Оплатить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
]
)
await callback_query.message.edit_text(
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
)
logger.info(f"Payment link sent to user {callback_query.from_user.id}.")
await callback_query.answer()
async def robokassa_webhook(request):
"""Обработка webhook-уведомлений от Robokassa."""
try:
params = await request.post()
logger.debug(f"Received webhook params: {params}")
if not check_payment_signature(params):
logger.error("Неверная подпись или данные запроса.")
return web.Response(status=400)
amount = params.get("OutSum")
inv_id = params.get("InvId")
if not amount or not inv_id:
logger.error("Отсутствуют обязательные параметры.")
return web.Response(status=400)
tg_id = inv_id
await update_balance(int(tg_id), float(amount))
await send_payment_success_notification(tg_id, float(amount))
logger.info(f"Payment successful. Balance updated for user {tg_id}.")
return web.Response(text=f"OK{inv_id}")
except Exception as e:
logger.error(f"Error processing webhook: {e}")
return web.Response(status=500)
def check_payment_signature(params):
"""Проверка подписи запроса от Robokassa."""
out_sum = params.get("OutSum")
inv_id = params.get("InvId")
signature_value = params.get("SignatureValue")
signature_string = f"{out_sum}:{inv_id}:{ROBOKASSA_PASSWORD2}"
expected_signature = (
hashlib.md5(signature_string.encode("utf-8")).hexdigest().upper()
)
logger.debug(f"Expected signature: {expected_signature}")
logger.debug(f"Received signature: {signature_value}")
return signature_value == expected_signature
async def send_payment_success_notification(user_id: int, amount: float):
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
logger.info(f"Sent payment success notification to user {user_id}.")
@router.callback_query(F.data == "enter_custom_amount_robokassa")
async def process_custom_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
logger.info(f"User {tg_id} chose to enter a custom amount.")
await callback_query.message.edit_text(
text="Пожалуйста, введите сумму пополнения в рублях (например, 150):"
)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_robokassa
)
await callback_query.answer()
@router.message(ReplenishBalanceState.waiting_for_payment_confirmation_robokassa)
async def handle_custom_amount_input(message: types.Message, state: FSMContext):
tg_id = message.from_user.id
logger.info(f"User {tg_id} entered custom amount: {message.text}")
try:
amount = int(message.text)
if amount <= 0:
raise ValueError("Сумма должна быть положительным числом.")
await state.update_data(amount=amount)
payment_url = generate_payment_link(
amount,
tg_id,
"Пополнение баланса",
)
logger.info(f"Generated payment link for user {tg_id}: {payment_url}")
confirm_keyboard = InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Оплатить", url=payment_url)],
[InlineKeyboardButton(text="⬅️ Назад", callback_data="pay_robokassa")],
]
)
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей. Для оплаты перейдите по ссылке ниже:",
reply_markup=confirm_keyboard,
)
await state.clear()
except ValueError as e:
logger.error(f"Некорректная сумма от пользователя {tg_id}: {e}")
await message.answer(
text="Введите корректную сумму в рублях (целое положительное число)."
)
+243
View File
@@ -0,0 +1,243 @@
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, LabeledPrice, PreCheckoutQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import RUB_TO_XTR
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
class ReplenishBalanceState(StatesGroup):
choosing_amount_stars = State()
waiting_for_payment_confirmation_stars = State()
entering_custom_amount_stars = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_stars")
async def process_callback_pay_stars(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'stars_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'stars_{PAYMENT_OPTIONS[i + 1]["callback_data"]}',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'stars_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_stars"
)
)
builder.row(
InlineKeyboardButton(
text="🤖 Бот для покупки звезд", url="https://t.me/PremiumBot"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"))
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)
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_stars)
await callback_query.answer()
@router.callback_query(F.data.startswith("stars_amount|"))
async def process_amount_selection(
callback_query: types.CallbackQuery, state: FSMContext
):
data = callback_query.data.split("|", 1)
if len(data) != 2:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Неверные данные для выбора суммы.")
return
amount_str = data[1]
try:
amount = int(amount_str)
except ValueError:
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
await callback_query.message.answer("Некорректная сумма.")
return
await state.update_data(amount=amount)
await state.set_state(ReplenishBalanceState.waiting_for_payment_confirmation_stars)
try:
await callback_query.message.delete()
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", pay=True),
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await callback_query.message.answer_invoice(
title=f"Вы выбрали пополнение на {amount} рублей.",
description=f"Вы выбрали пополнение на {amount} рублей.",
prices=[LabeledPrice(label="XTR", amount=int(amount // RUB_TO_XTR))],
provider_token="",
payload=f"{amount}_stars",
currency="XTR",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await callback_query.message.answer("Произошла ошибка при создании платежа.")
await callback_query.answer()
async def send_payment_success_notification(user_id: int, amount: float):
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
@router.callback_query(F.data == "enter_custom_amount_stars")
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_stars)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_stars)
async def process_custom_amount_input(message: types.Message, state: FSMContext):
if message.text.isdigit():
amount = int(message.text)
if amount // RUB_TO_XTR <= 0:
await message.answer(
f"Сумма должна быть больше {RUB_TO_XTR}. Пожалуйста, введите сумму еще раз:"
)
return
await state.update_data(amount=amount)
await state.set_state(
ReplenishBalanceState.waiting_for_payment_confirmation_stars
)
try:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Пополнить", pay=True),
)
builder.row(
InlineKeyboardButton(text="⬅️ Назад", callback_data="pay"),
)
await message.answer_invoice(
title=f"Вы выбрали пополнение на {amount} рублей.",
description=f"Вы выбрали пополнение на {amount} рублей.",
prices=[LabeledPrice(label="XTR", amount=int(amount // RUB_TO_XTR))],
provider_token="",
payload=f"{amount}_stars",
currency="XTR",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
@router.pre_checkout_query()
async def on_pre_checkout_query(pre_checkout_query: PreCheckoutQuery):
await pre_checkout_query.answer(ok=True)
@router.message(F.successful_payment)
async def on_successful_payment(
message: types.Message,
):
try:
user_id = int(message.from_user.id)
amount = float(message.successful_payment.invoice_payload.split("_")[0])
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
+310
View File
@@ -0,0 +1,310 @@
import uuid
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from aiohttp import web
from yookassa import Configuration, Payment
from bot import bot
from config import YOOKASSA_ENABLE, YOOKASSA_SECRET_KEY, YOOKASSA_SHOP_ID
from database import add_connection, check_connection_exists, get_key_count, update_balance
from handlers.texts import PAYMENT_OPTIONS
from logger import logger
router = Router()
if YOOKASSA_ENABLE:
Configuration.account_id = YOOKASSA_SHOP_ID
Configuration.secret_key = YOOKASSA_SECRET_KEY
logger.debug(f"Account ID: {YOOKASSA_SHOP_ID}")
logger.debug(f"Secret Key: {YOOKASSA_SECRET_KEY}")
class ReplenishBalanceState(StatesGroup):
choosing_amount_yookassa = State()
waiting_for_payment_confirmation_yookassa = State()
entering_custom_amount_yookassa = State()
async def send_message_with_deletion(
chat_id, text, reply_markup=None, state=None, message_key="last_message_id"
):
if state:
try:
state_data = await state.get_data()
previous_message_id = state_data.get(message_key)
if previous_message_id:
await bot.delete_message(
chat_id=chat_id, message_id=previous_message_id
)
sent_message = await bot.send_message(
chat_id=chat_id, text=text, reply_markup=reply_markup
)
await state.update_data({message_key: sent_message.message_id})
except Exception as e:
logger.error(f"Ошибка при удалении/отправке сообщения: {e}")
return None
return sent_message
@router.callback_query(F.data == "pay_yookassa")
async def process_callback_pay_yookassa(
callback_query: types.CallbackQuery, state: FSMContext
):
tg_id = callback_query.from_user.id
builder = InlineKeyboardBuilder()
for i in range(0, len(PAYMENT_OPTIONS), 2):
if i + 1 < len(PAYMENT_OPTIONS):
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'yookassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
),
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i + 1]["text"],
callback_data=f'yookassa_{PAYMENT_OPTIONS[i + 1]["callback_data"]},',
),
)
else:
builder.row(
InlineKeyboardButton(
text=PAYMENT_OPTIONS[i]["text"],
callback_data=f'yookassa_{PAYMENT_OPTIONS[i]["callback_data"]}',
)
)
builder.row(
InlineKeyboardButton(
text="💰 Ввести свою сумму", callback_data="enter_custom_amount_yookassa"
)
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="view_profile"))
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)
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception as e:
logger.error(f"Не удалось удалить сообщение: {e}")
await bot.send_message(
chat_id=tg_id,
text="Выберите сумму пополнения:",
reply_markup=builder.as_markup(),
)
await state.set_state(ReplenishBalanceState.choosing_amount_yookassa)
await callback_query.answer()
@router.callback_query(F.data.startswith("yookassa_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_yookassa
)
# 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="pay")],
]
)
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:
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="Перейти в профиль", callback_data="view_profile")
)
await bot.send_message(
chat_id=user_id,
text=f"Ваш баланс успешно пополнен на {amount} рублей. Спасибо за оплату!",
reply_markup=builder.as_markup(),
)
except Exception as e:
logger.error(f"Ошибка при отправке уведомления пользователю {user_id}: {e}")
async def yookassa_webhook(request):
event = await request.json()
logger.debug(f"Webhook event received: {event}")
if event["event"] == "payment.succeeded":
user_id_str = event["object"]["metadata"]["user_id"]
amount_str = event["object"]["amount"]["value"]
try:
user_id = int(user_id_str)
amount = float(amount_str)
logger.debug(f"Payment succeeded for user_id: {user_id}, amount: {amount}")
await update_balance(user_id, amount)
await send_payment_success_notification(user_id, amount)
except ValueError as e:
logger.error(f"Ошибка конвертации user_id или amount: {e}")
return web.Response(status=400)
return web.Response(status=200)
@router.callback_query(F.data == "enter_custom_amount_yookassa")
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_yookassa)
await callback_query.answer()
@router.message(ReplenishBalanceState.entering_custom_amount_yookassa)
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_yookassa
)
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="pay")],
]
)
await message.answer(
text=f"Вы выбрали пополнение на {amount} рублей.",
reply_markup=confirm_keyboard,
)
else:
await message.answer("Ошибка при создании платежа.")
except Exception as e:
logger.error(f"Ошибка при создании платежа: {e}")
await message.answer("Произошла ошибка при создании платежа.")
else:
await message.answer("Некорректная сумма. Пожалуйста, введите сумму еще раз:")
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+107 -67
View File
@@ -1,103 +1,143 @@
import os
from aiogram import Router, types
from aiogram import F, Router, types
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, BufferedInputFile
from aiogram.types import BufferedInputFile, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CHANNEL_URL
from database import get_balance, get_key_count, get_referral_stats
from handlers.texts import profile_message_send, invite_message_send, CHANNEL_LINK, get_referral_link
from config import PAYMENT_METHOD
import logging
class ReplenishBalanceState(StatesGroup):
choosing_transfer_method = State()
waiting_for_admin_confirmation = State()
from handlers.texts import get_referral_link, invite_message_send, profile_message_send
from logger import logger
router = Router()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext):
tg_id = callback_query.from_user.id
username = callback_query.from_user.full_name
async def process_callback_view_profile(
callback_query: types.CallbackQuery, state: FSMContext
):
chat_id = callback_query.from_user.id
username = callback_query.from_user.full_name
image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg')
if not os.path.isfile(image_path):
await bot.send_message(tg_id, "Файл изображения не найден.")
return
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
try:
key_count = await get_key_count(tg_id)
balance = await get_balance(tg_id)
key_count = await get_key_count(chat_id)
balance = await get_balance(chat_id)
if balance is None:
balance = 0
balance = 0
profile_message = profile_message_send(username, chat_id, balance, key_count)
profile_message = profile_message_send(username, tg_id, balance, key_count)
profile_message += (
f"<b>Обязательно подпишитесь на канал</b> <a href='{CHANNEL_LINK}'>здесь</a>\n"
)
if key_count == 0:
profile_message += "\n<i>Нажмите Устройство снизу, чтобы добавить устройство в VPN</i>"
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text=' Устройство', callback_data='create_key'), InlineKeyboardButton(text='📱 Мои устр-ва', callback_data='view_keys')],
[InlineKeyboardButton(text='💳 Пополнить баланс', callback_data='pay_freekassa' if PAYMENT_METHOD == 'freekassa' else 'replenish_balance')],
[InlineKeyboardButton(text='👥 Пригласить', callback_data='invite'), InlineKeyboardButton(text='📘 Инструкции', callback_data='instructions')],
[InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')]
])
profile_message += "\n🔧 <i>Нажмите кнопку Устройство, чтобы настроить VPN-подключение</i>"
builder = InlineKeyboardBuilder()
builder.row(InlineKeyboardButton(text="📢 Наш канал", url=CHANNEL_URL))
builder.row(
InlineKeyboardButton(text=" Устройство", callback_data="create_key"),
InlineKeyboardButton(text="📱 Мои устройства", callback_data="view_keys"),
)
builder.row(
InlineKeyboardButton(
text="💳 Пополнить баланс",
callback_data="pay",
)
)
builder.row(
InlineKeyboardButton(text="👥 Пригласить друзей", callback_data="invite"),
InlineKeyboardButton(text="📘 Инструкции", callback_data="instructions"),
)
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(
InlineKeyboardButton(text="⬅️ Главное меню", callback_data="back_to_menu")
)
# Попробуем удалить предыдущее сообщение
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}") # Логируем ошибку, если удаление не удалось
logger.error(f"Ошибка при удалении сообщения: {e}")
with open(image_path, 'rb') as image_file:
await bot.send_photo(
chat_id=tg_id,
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
parse_mode='HTML',
reply_markup=inline_keyboard
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(image_file.read(), filename="pic.jpg"),
caption=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=profile_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(tg_id, f"❗️ Ошибка при получении данных профиля: {e}")
await bot.send_message(
chat_id, f"❗️ Не удалось загрузить профиль. Техническая ошибка: {e}"
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'invite')
@router.callback_query(F.data == "invite")
async def invite_handler(callback_query: types.CallbackQuery):
tg_id = callback_query.from_user.id
referral_link = get_referral_link(tg_id)
referral_stats = await get_referral_stats(tg_id)
invite_message = (
invite_message_send(referral_link,referral_stats)
)
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='view_profile')
keyboard = InlineKeyboardMarkup(inline_keyboard=[[button_back]])
chat_id = callback_query.from_user.id
referral_link = get_referral_link(chat_id)
await callback_query.message.delete()
referral_stats = await get_referral_stats(chat_id)
await bot.send_message(
chat_id=tg_id,
text=invite_message,
parse_mode='HTML',
reply_markup=keyboard
invite_message = invite_message_send(referral_link, referral_stats)
image_path = os.path.join(os.path.dirname(__file__), "pic_invite.jpg")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="⬅️ Вернуться в профиль", callback_data="view_profile")
)
try:
await callback_query.message.delete()
except Exception as e:
logger.error(f"Ошибка при удалении сообщения: {e}")
try:
if os.path.isfile(image_path):
with open(image_path, "rb") as image_file:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(
image_file.read(), filename="pic_invite.jpg"
),
caption=invite_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=invite_message,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
except Exception as e:
await bot.send_message(
chat_id=chat_id,
text=f"❗️ Не удалось отправить сообщение. Техническая ошибка: {e}",
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'view_profile')
@router.callback_query(F.data == "view_profile")
async def view_profile_handler(callback_query: types.CallbackQuery, state: FSMContext):
await state.clear()
await process_callback_view_profile(callback_query, state)
+104 -69
View File
@@ -1,120 +1,155 @@
import os
from aiogram import Router
from aiogram import F, Router
from aiogram.filters import Command
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import (BufferedInputFile, CallbackQuery,
InlineKeyboardButton, InlineKeyboardMarkup, Message)
from handlers.texts import ABOUT_VPN, WELCOME_TEXT
from aiogram.types import BufferedInputFile, CallbackQuery, InlineKeyboardButton, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
from bot import bot
from config import CHANNEL_URL, SUPPORT_CHAT_URL, APP_URL
from config import CHANNEL_URL, CONNECT_ANDROID, CONNECT_IOS, DOWNLOAD_ANDROID, DOWNLOAD_IOS, SUPPORT_CHAT_URL
from database import add_connection, add_referral, check_connection_exists, get_trial
from handlers.keys.trial_key import create_trial_key
from handlers.texts import INSTRUCTIONS_TRIAL
from handlers.keys.trial_key import create_trial_key
from handlers.texts import INSTRUCTIONS_TRIAL, WELCOME_TEXT, get_about_vpn
from logger import logger
router = Router()
class FeedbackState(StatesGroup):
waiting_for_feedback = State()
async def send_welcome_message(chat_id: int, trial_status: int):
welcome_text = WELCOME_TEXT
image_path = os.path.join(os.path.dirname(__file__), "pic.jpg")
image_path = os.path.join(os.path.dirname(__file__), 'pic.jpg')
builder = InlineKeyboardBuilder()
if trial_status == 0:
builder.row(
InlineKeyboardButton(text="🔗 Подключить VPN", callback_data="connect_vpn")
)
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
)
builder.row(
InlineKeyboardButton(text="📞 Техническая поддержка", url=SUPPORT_CHAT_URL),
)
builder.row(
InlineKeyboardButton(text="📢 Официальный канал", url=CHANNEL_URL),
)
builder.row(InlineKeyboardButton(text="🌐 О нашем VPN", callback_data="about_vpn"))
if not os.path.isfile(image_path):
await bot.send_message(chat_id, "Файл изображения не найден.")
return
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text='🔗 Подключить VPN', callback_data='connect_vpn')] if trial_status == 0 else [],
[InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')],
[InlineKeyboardButton(text='🔒 О VPN', callback_data='about_vpn')],
[InlineKeyboardButton(text='📞 Поддержка', url=SUPPORT_CHAT_URL)],
[InlineKeyboardButton(text='📢 Наш канал', url=CHANNEL_URL)],
])
inline_keyboard.inline_keyboard = [row for row in inline_keyboard.inline_keyboard if row]
with open(image_path, 'rb') as image_from_buffer:
await bot.send_photo(
chat_id,
BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
caption=welcome_text,
parse_mode='HTML',
reply_markup=inline_keyboard
if os.path.isfile(image_path):
with open(image_path, "rb") as image_from_buffer:
await bot.send_photo(
chat_id=chat_id,
photo=BufferedInputFile(image_from_buffer.read(), filename="pic.jpg"),
caption=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
else:
await bot.send_message(
chat_id=chat_id,
text=WELCOME_TEXT,
parse_mode="HTML",
reply_markup=builder.as_markup(),
)
@router.message(Command('start'))
@router.message(Command("start"))
async def start_command(message: Message):
print(f"Received start command with text: {message.text}")
if 'referral_' in message.text:
referrer_tg_id = int(message.text.split('referral_')[1])
print(f"Referral ID: {referrer_tg_id}")
logger.info(f"Received start command with text: {message.text}")
if "referral_" in message.text:
referrer_tg_id = int(message.text.split("referral_")[1])
logger.info(f"Referral ID: {referrer_tg_id}")
if not await check_connection_exists(message.from_user.id):
await add_connection(message.from_user.id)
await add_referral(message.from_user.id, referrer_tg_id)
await message.answer("Вас пригласил друг, добро пожаловать!")
else:
await message.answer("Вы уже зарегистрированы в системе!")
trial_status = await get_trial(message.from_user.id)
await send_welcome_message(message.chat.id, trial_status)
@router.callback_query(lambda c: c.data == 'connect_vpn')
async def handle_connect_vpn(callback_query: CallbackQuery):
@router.callback_query(F.data == "connect_vpn")
async def handle_connect_vpn(callback_query: CallbackQuery, session):
await callback_query.message.delete()
user_id = callback_query.from_user.id
trial_key_info = await create_trial_key(user_id)
if 'error' in trial_key_info:
await callback_query.message.answer(trial_key_info['error'])
if "error" in trial_key_info:
await callback_query.message.answer(trial_key_info["error"])
else:
try:
result = await session.execute(
"""
UPDATE connections SET trial = 1 WHERE tg_id = $1
""",
user_id,
)
logger.info(f"Rows updated: {result}")
except Exception as e:
logger.error(f"Ошибка при обновлении trial: {e}")
key_message = (
f"<b>Ваш ключ доступа:</b>\n<pre>{trial_key_info['key']}</pre>\n\n"
f"<b>Инструкции:</b>\n{INSTRUCTIONS_TRIAL}"
f"🔑 <b>Ваш персональный ключ доступа:</b>\n"
f"<pre>{trial_key_info['key']}</pre>\n\n"
f"📋 <b>Быстрая инструкция по подключению:</b>\n{INSTRUCTIONS_TRIAL}"
)
button_profile = InlineKeyboardButton(text='👤 Мой профиль', callback_data='view_profile')
button_iphone = InlineKeyboardButton(
text='🍏IPhone',
url=f'{APP_URL}/?url=v2raytun://import/{trial_key_info["key"]}'
)
button_android = InlineKeyboardButton(
text='🤖Android',
url=f'{APP_URL}/?url=v2raytun://import-sub?url={trial_key_info["key"]}'
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="👤 Личный кабинет", callback_data="view_profile")
)
inline_keyboard = InlineKeyboardMarkup(inline_keyboard=[
[button_iphone, button_android],
[button_profile]
])
builder.row(
InlineKeyboardButton(
text="🍏 Скачать для iOS",
url=DOWNLOAD_IOS,
),
InlineKeyboardButton(
text="🤖 Скачать для Android",
url=DOWNLOAD_ANDROID,
),
)
builder.row(
InlineKeyboardButton(
text="🍏 Подключить на iOS",
url=f'{CONNECT_IOS}{trial_key_info["key"]}',
),
InlineKeyboardButton(
text="🤖 Подключить на Android",
url=f'{CONNECT_ANDROID}{trial_key_info["key"]}',
),
)
await callback_query.message.answer(
key_message,
parse_mode='HTML',
reply_markup=inline_keyboard
key_message, parse_mode="HTML", reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'about_vpn')
@router.callback_query(F.data == "about_vpn")
async def handle_about_vpn(callback_query: CallbackQuery):
await callback_query.message.delete()
info_message = ABOUT_VPN
button_back = InlineKeyboardButton(text='⬅️ Назад', callback_data='back_to_menu')
inline_keyboard_back = InlineKeyboardMarkup(inline_keyboard=[[button_back]])
about_vpn_message = get_about_vpn("3.1.0_Stable")
builder = InlineKeyboardBuilder()
builder.row(
InlineKeyboardButton(text="💰 Поддержать проект", callback_data="donate")
)
builder.row(InlineKeyboardButton(text="⬅️ Назад", callback_data="back_to_menu"))
await callback_query.message.answer(
info_message,
parse_mode='HTML',
reply_markup=inline_keyboard_back
about_vpn_message, parse_mode="HTML", reply_markup=builder.as_markup()
)
await callback_query.answer()
@router.callback_query(lambda c: c.data == 'back_to_menu')
@router.callback_query(F.data == "back_to_menu")
async def handle_back_to_menu(callback_query: CallbackQuery):
await callback_query.message.delete()
trial_status = await get_trial(callback_query.from_user.id)
+73 -16
View File
@@ -1,24 +1,81 @@
import re
import random
from config import SERVERS
import re
import asyncpg
from bot import bot
from config import CLUSTERS, DATABASE_URL
from logger import logger
def sanitize_key_name(key_name: str) -> str:
return re.sub(r'[^a-z0-9@._-]', '', key_name.lower())
return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
def generate_random_email():
"""Генерирует случайный набор символов."""
random_string = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz0123456789', k=6))
return random_string
random_string = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=6))
return random_string
async def get_least_loaded_server(conn):
"""Находит сервер с наименьшей загрузкой."""
least_loaded_server_id = None
min_load_percentage = float('inf')
for server_id, server in SERVERS.items():
count = await conn.fetchval('SELECT COUNT(*) FROM keys WHERE server_id = $1', server_id)
percent_full = (count / 60) * 100 if count <= 60 else 100
if percent_full < min_load_percentage:
min_load_percentage = percent_full
least_loaded_server_id = server_id
async def get_least_loaded_cluster():
"""
Функция для получения кластера с наименьшей загрузкой (по количеству ключей).
Возвращает идентификатор кластера с наименьшей загрузкой или первый кластер из конфигурации,
если загруженность не определяется. В случае отсутствия кластеров с номером, возвращает 'cluster1'.
"""
cluster_loads = {}
return least_loaded_server_id
conn = await asyncpg.connect(DATABASE_URL)
try:
keys = await conn.fetch("SELECT * FROM keys")
for key in keys:
cluster_id = key["server_id"]
if re.match(r"^cluster\d+$", cluster_id):
if cluster_id not in cluster_loads:
cluster_loads[cluster_id] = 0
cluster_loads[cluster_id] += 1
finally:
await conn.close()
logger.info(f"Cluster loads: {cluster_loads}")
if not cluster_loads:
available_clusters = [
cluster_id
for cluster_id in CLUSTERS.keys()
if re.match(r"^cluster\d+$", cluster_id)
]
logger.info(f"Available clusters from config: {available_clusters}")
if available_clusters:
logger.info(
f"Returning the first available cluster: {available_clusters[0]}"
)
return available_clusters[0]
else:
logger.warning("No valid clusters found in config, returning 'cluster1'.")
return "cluster1"
least_loaded_cluster = min(cluster_loads, key=cluster_loads.get)
logger.info(f"Least loaded cluster selected: {least_loaded_cluster}")
return least_loaded_cluster
async def handle_error(tg_id, callback_query, message):
try:
try:
await bot.delete_message(
chat_id=tg_id, message_id=callback_query.message.message_id
)
except Exception:
pass
await bot.send_message(tg_id, message, parse_mode="HTML")
except Exception as e:
logger.error(f"Ошибка при обработке ошибки: {e}")
+47
View File
@@ -0,0 +1,47 @@
import logging
import sys
from loguru import logger
logger.remove()
level_mapping = {
50: "CRITICAL",
40: "ERROR",
30: "WARNING",
20: "INFO",
10: "DEBUG",
0: "NOTSET",
}
class InterceptHandler(logging.Handler):
def emit(self, record):
logger_opt = logger.opt(depth=6, exception=record.exc_info)
message = record.getMessage()
logger_opt.log(level_mapping.get(record.levelno, "INFO"), message)
logging.basicConfig(handlers=[InterceptHandler()], level=0)
logging.getLogger("httpcore").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
# Настройка вывода в консоль
logger.add(
sys.stderr,
level="INFO",
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level}</level> | <cyan>{module}:{function}:{line}</cyan> | <level>{message}</level>",
colorize=True,
)
# Настройка записи в файл
logger.add(
"logging.log",
level="DEBUG",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {module}:{function}:{line} | {message}",
rotation="60 minute",
retention=24,
)
logger = logger
+61 -38
View File
@@ -1,37 +1,41 @@
import asyncio
import logging
import signal
from aiogram.webhook.aiohttp_server import (SimpleRequestHandler,
setup_application)
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
from aiohttp import web
from backup import backup_database
from bot import bot, dp, router
from config import WEBAPP_HOST, WEBAPP_PORT, WEBHOOK_PATH, WEBHOOK_URL
from config import CRYPTO_BOT_ENABLE, DEV_MODE, FREEKASSA_ENABLE, ROBOKASSA_ENABLE, SUB_PATH, WEBAPP_HOST, WEBAPP_PORT, WEBHOOK_PATH, WEBHOOK_URL, YOOKASSA_ENABLE
from database import init_db
from handlers.keys.subscriptions import handle_subscription
from handlers.notifications import notify_expiring_keys
from handlers.payment.pay import payment_webhook
from handlers.payment.freekassa import freekassa_webhook
from handlers.payments.cryprobot_pay import cryptobot_webhook
from handlers.payments.freekassa_pay import freekassa_webhook
from handlers.payments.robokassa_pay import robokassa_webhook
from handlers.payments.yookassa_pay import yookassa_webhook
from logger import logger
logging.basicConfig(level=logging.DEBUG)
async def periodic_notifications():
while True:
await notify_expiring_keys(bot)
await asyncio.sleep(3600)
await asyncio.sleep(1800)
async def periodic_database_backup():
while True:
await backup_database()
await asyncio.sleep(21600)
await backup_database()
await asyncio.sleep(21600)
async def on_startup(app):
await bot.set_webhook(WEBHOOK_URL)
await init_db()
asyncio.create_task(periodic_notifications())
asyncio.create_task(periodic_notifications())
asyncio.create_task(periodic_database_backup())
async def on_shutdown(app):
await bot.delete_webhook()
for task in asyncio.all_tasks():
@@ -39,46 +43,65 @@ async def on_shutdown(app):
try:
await asyncio.gather(*asyncio.all_tasks(), return_exceptions=True)
except Exception as e:
logging.error(f"Error during shutdown: {e}")
logger.error(f"Error during shutdown: {e}")
async def shutdown_site(site):
logging.info("Остановка сайта...")
logger.info("Остановка сайта...")
await site.stop()
logging.info("Сервер остановлен.")
logger.info("Сервер остановлен.")
async def main():
dp.include_router(router)
app = web.Application()
app.on_startup.append(on_startup)
app.on_shutdown.append(on_shutdown)
app.router.add_post('/yookassa/webhook', payment_webhook)
app.router.add_post('/freekassa/webhook', freekassa_webhook)
if DEV_MODE:
# Запуск в режиме полинга для разработки
await bot.delete_webhook()
await init_db()
await dp.start_polling(bot)
else:
# Стандартный режим вебхука
app = web.Application()
app.on_startup.append(on_startup)
app.on_shutdown.append(on_shutdown)
if YOOKASSA_ENABLE:
app.router.add_post("/yookassa/webhook", yookassa_webhook)
if FREEKASSA_ENABLE:
app.router.add_post("/freekassa/webhook", freekassa_webhook)
if CRYPTO_BOT_ENABLE:
app.router.add_post("/cryptobot/webhook", cryptobot_webhook)
if ROBOKASSA_ENABLE:
app.router.add_post("/robokassa/webhook", robokassa_webhook)
app.router.add_get(f"{SUB_PATH}{{email}}", handle_subscription)
SimpleRequestHandler(dispatcher=dp, bot=bot).register(app, path=WEBHOOK_PATH)
setup_application(app, dp, bot=bot)
SimpleRequestHandler(dispatcher=dp, bot=bot).register(app, path=WEBHOOK_PATH)
setup_application(app, dp, bot=bot)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host=WEBAPP_HOST, port=WEBAPP_PORT)
await site.start()
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host=WEBAPP_HOST, port=WEBAPP_PORT)
await site.start()
print(f"Webhook URL: {WEBHOOK_URL}")
logger.info(f"Webhook URL: {WEBHOOK_URL}")
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown_site(site)))
loop = asyncio.get_event_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
loop.add_signal_handler(
sig, lambda: asyncio.create_task(shutdown_site(site))
)
try:
await asyncio.Event().wait()
finally:
pending = asyncio.all_tasks()
for task in pending:
task.cancel()
await asyncio.gather(*pending, return_exceptions=True)
try:
await asyncio.Event().wait()
finally:
pending = asyncio.all_tasks()
for task in pending:
task.cancel()
await asyncio.gather(*pending, return_exceptions=True)
if __name__ == '__main__':
if __name__ == "__main__":
try:
asyncio.run(main())
except Exception as e:
logging.error(f"Ошибка при запуске приложения: {e}")
logger.error(f"Ошибка при запуске приложения:\n{e}")
+22
View File
@@ -0,0 +1,22 @@
from typing import Any, Awaitable, Callable, Dict
import asyncpg
from aiogram import BaseMiddleware
from aiogram.types import TelegramObject
from config import DATABASE_URL
class DatabaseMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
session = await asyncpg.connect(DATABASE_URL)
data["session"] = session
try:
return await handler(event, data)
finally:
await session.close()
+35
View File
@@ -0,0 +1,35 @@
from typing import Any, Awaitable, Callable, Dict
from aiogram import BaseMiddleware
from aiogram.types import CallbackQuery, Message, TelegramObject
from logger import logger
class LoggingMiddleware(BaseMiddleware):
async def __call__(
self,
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
event: TelegramObject,
data: Dict[str, Any],
) -> Any:
user_id = None
username = None
action = None
if isinstance(event, Message):
user_id = event.from_user.id
username = event.from_user.username
action = f"Сообщение: {event.text}"
elif isinstance(event, CallbackQuery):
user_id = event.from_user.id
username = event.from_user.username
action = f"Обратный вызов: {event.data}"
logger.info(
f"Активность пользователя - "
f"ID пользователя: {user_id}, "
f"Имя пользователя: {username}, "
f"Действие: {action}"
)
return await handler(event, data)
+70
View File
@@ -0,0 +1,70 @@
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class Connection(Base):
"""
Модель для таблицы connections
"""
__tablename__ = "connections"
tg_id = Column(BigInteger, primary_key=True, nullable=False)
balance = Column(Float, nullable=False, default=0.0)
trial = Column(BigInteger, nullable=False, default=0)
# Связь с ключами и рефералами
keys = relationship("Key", back_populates="connection")
referrals_received = relationship(
"Referral", foreign_keys="Referral.referred_tg_id", back_populates="referred"
)
referrals_sent = relationship(
"Referral", foreign_keys="Referral.referrer_tg_id", back_populates="referrer"
)
class Key(Base):
"""
Модель для таблицы keys
"""
__tablename__ = "keys"
tg_id = Column(
BigInteger, ForeignKey("connections.tg_id"), primary_key=True, nullable=False
)
client_id = Column(Text, primary_key=True, nullable=False)
email = Column(Text, nullable=False)
created_at = Column(BigInteger, nullable=False)
expiry_time = Column(BigInteger, nullable=False)
key = Column(Text, nullable=False)
server_id = Column(Text, nullable=False, default="server1")
notified = Column(Boolean, nullable=False, default=False)
notified_24h = Column(Boolean, nullable=False, default=False)
# Связь с подключением
connection = relationship("Connection", back_populates="keys")
class Referral(Base):
"""
Модель для таблицы referrals
"""
__tablename__ = "referrals"
referred_tg_id = Column(
BigInteger, ForeignKey("connections.tg_id"), primary_key=True, nullable=False
)
referrer_tg_id = Column(BigInteger, ForeignKey("connections.tg_id"), nullable=False)
reward_issued = Column(Boolean, default=False)
# Связи с подключениями
referred = relationship(
"Connection", foreign_keys=[referred_tg_id], back_populates="referrals_received"
)
referrer = relationship(
"Connection", foreign_keys=[referrer_tg_id], back_populates="referrals_sent"
)
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

+7 -3
View File
@@ -7,7 +7,7 @@ annotated-types==0.7.0
async-timeout==4.0.3
asyncpg==0.30.0
attrs==24.2.0
certifi==2024.8.30
certifi
charset-normalizer==3.4.0
Deprecated==1.2.14
distro==1.9.0
@@ -17,11 +17,15 @@ magic-filter==1.0.12
multidict==6.1.0
netaddr==1.3.0
propcache==0.2.0
pydantic==2.9.2
pydantic_core==2.23.4
pydantic
pydantic_core
requests==2.32.3
typing_extensions==4.12.2
urllib3==2.2.3
wrapt==1.16.0
yarl==1.15.5
yookassa==3.3.0
loguru
aiocryptopay
py3xui
sqlalchemy
+164
View File
@@ -0,0 +1,164 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}3x-ui Installation Script by izzzzzi${NC}"
echo "----------------------------------------"
# Запрос данных у пользователя
read -p "Enter your domain (e.g., example.com): " DOMAIN
read -p "Enter desired admin panel port (default: 2053): " PANEL_PORT
read -p "Enter desired sub panel port (default: 2054): " SUB_PANEL_PORT
# Использование значения по умолчанию для порта, если не указано
PANEL_PORT=${PANEL_PORT:-2053}
SUB_PANEL_PORT=${SUB_PANEL_PORT:-2054}
# Создание необходимых директорий
echo -e "${GREEN}Creating directories...${NC}"
mkdir -p nginx/conf.d db cert
# Установка certbot
echo -e "${GREEN}Installing certbot...${NC}"
apt-get update
apt-get install certbot -y
# Остановка nginx если он запущен
echo -e "${GREEN}Stopping nginx if running...${NC}"
docker-compose down 2>/dev/null
systemctl stop nginx 2>/dev/null
# Получение сертификата
echo -e "${GREEN}Obtaining SSL certificate...${NC}"
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d $DOMAIN
# Проверка успешности получения сертификата
if [ ! -d "/etc/letsencrypt/live/$DOMAIN" ]; then
echo "Failed to obtain SSL certificate. Please check your domain settings."
exit 1
fi
# Тестирование автообновления
echo -e "${GREEN}Testing certificate renewal...${NC}"
certbot renew --dry-run
# Создание docker-compose.yml
echo -e "${GREEN}Creating docker-compose.yml...${NC}"
cat > docker-compose.yml << EOL
version: "3"
services:
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt
- /var/lib/letsencrypt:/var/lib/letsencrypt
networks:
- proxy-network
restart: unless-stopped
depends_on:
- 3x-ui
3x-ui:
image: ghcr.io/mhsanaei/3x-ui:latest
container_name: 3x-ui
hostname: ${DOMAIN}
volumes:
- \$PWD/db/:/etc/x-ui/
- \$PWD/cert/:/root/cert/
- /etc/letsencrypt:/etc/letsencrypt:ro
environment:
XRAY_VMESS_AEAD_FORCED: "false"
tty: true
networks:
- proxy-network
restart: unless-stopped
networks:
proxy-network:
driver: bridge
EOL
# Создание конфигурации Nginx
echo -e "${GREEN}Creating Nginx configuration...${NC}"
cat > nginx/conf.d/default.conf << EOL
server {
listen 80;
server_name ${DOMAIN};
location / {
return 301 https://\$host\$request_uri;
}
}
server {
listen 443 ssl;
server_name ${DOMAIN};
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
location / {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Range \$http_range;
proxy_set_header If-Range \$http_if_range;
proxy_redirect off;
proxy_pass http://3x-ui:${PANEL_PORT};
}
location /sub {
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header Range \$http_range;
proxy_set_header If-Range \$http_if_range;
proxy_redirect off;
proxy_pass http://3x-ui:${SUB_PANEL_PORT};
}
}
EOL
# Проверка наличия Docker и Docker Compose
if ! command -v docker &> /dev/null || ! command -v docker-compose &> /dev/null; then
echo -e "${GREEN}Installing Docker and Docker Compose...${NC}"
curl -fsSL https://get.docker.com | sh
curl -L "https://github.com/docker/compose/releases/download/v2.12.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
fi
# Настройка автообновления сертификатов
echo -e "${GREEN}Setting up certificate auto-renewal...${NC}"
cat > /etc/cron.d/certbot-renew << EOL
0 */12 * * * root certbot renew --quiet --deploy-hook "docker-compose -f $(pwd)/docker-compose.yml restart nginx"
EOL
# Запуск сервисов
echo -e "${GREEN}Starting all services...${NC}"
docker-compose up -d
echo -e "${GREEN}Installation completed!${NC}"
echo -e "${BLUE}You can access your 3x-ui panel at: https://${DOMAIN}${NC}"
echo -e "${YELLOW}Important: Default access to 3x-ui panel - ${NC}"
echo -e "${YELLOW}Login: admin${NC}"
echo -e "${YELLOW}Password: admin${NC}"
echo -e "${YELLOW}For security, it is recommended to change the password at: https://${DOMAIN}/panel/settings${NC}"
echo "Please wait a few minutes for all services to start properly."
echo "Default credentials can be found in the 3x-ui documentation."
EOL
+108
View File
@@ -0,0 +1,108 @@
#!/bin/bash
# Цвета для вывода
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
NC='\033[0m' # No Color
echo -e "${BLUE}PostgreSQL Installation and Setup Script by izzzzzi${NC}"
echo "----------------------------------------"
# Запрос данных у пользователя
read -p "Enter PostgreSQL port (default: 5432): " DB_PORT
read -p "Enter database name: " DB_NAME
read -p "Enter database user: " DB_USER
read -s -p "Enter database password: " DB_PASS
echo
read -s -p "Confirm database password: " DB_PASS_CONFIRM
echo
# Проверка паролей
if [ "$DB_PASS" != "$DB_PASS_CONFIRM" ]; then
echo -e "${RED}Passwords do not match!${NC}"
exit 1
fi
# Использование значения по умолчанию для порта
DB_PORT=${DB_PORT:-5432}
# Установка PostgreSQL
echo -e "${GREEN}Installing PostgreSQL...${NC}"
apt-get update
apt-get install -y postgresql postgresql-contrib
# Остановка PostgreSQL для изменения конфигурации
systemctl stop postgresql
# Настройка PostgreSQL для внешних подключений
echo -e "${GREEN}Configuring PostgreSQL...${NC}"
# Настройка postgresql.conf
PG_VERSION=$(ls /etc/postgresql/)
PG_CONF="/etc/postgresql/$PG_VERSION/main/postgresql.conf"
PG_HBA="/etc/postgresql/$PG_VERSION/main/pg_hba.conf"
# Backup конфигурационных файлов
cp $PG_CONF "${PG_CONF}.backup"
cp $PG_HBA "${PG_HBA}.backup"
# Изменение postgresql.conf
sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" $PG_CONF
sed -i "s/#port = 5432/port = $DB_PORT/" $PG_CONF
# Изменение pg_hba.conf
cat > $PG_HBA << EOL
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
local all all peer
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
host all all 0.0.0.0/0 md5
EOL
# Запуск PostgreSQL
systemctl start postgresql
systemctl enable postgresql
# Создание пользователя и базы данных
echo -e "${GREEN}Creating database and user...${NC}"
sudo -u postgres psql << EOF
CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';
CREATE DATABASE $DB_NAME OWNER $DB_USER;
ALTER USER $DB_USER WITH SUPERUSER;
EOF
# Настройка файрвола
echo -e "${GREEN}Configuring firewall...${NC}"
if command -v ufw >/dev/null; then
ufw allow $DB_PORT/tcp
ufw status
fi
# Проверка статуса PostgreSQL
systemctl status postgresql --no-pager
echo -e "${GREEN}Installation completed!${NC}"
echo -e "${BLUE}PostgreSQL is running on port: ${DB_PORT}${NC}"
echo -e "${BLUE}Database name: ${DB_NAME}${NC}"
echo -e "${BLUE}Database user: ${DB_USER}${NC}"
echo "You can now connect to your database using:"
echo "psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME"
echo
echo "For remote connections use:"
echo "psql -h YOUR_SERVER_IP -p $DB_PORT -U $DB_USER -d $DB_NAME"
echo
echo -e "${RED}Important: Make sure to save these credentials in a secure place!${NC}"
# Проверка подключения
echo -e "${GREEN}Testing connection...${NC}"
PGPASSWORD=$DB_PASS psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME -c "\conninfo"
# Добавление информации о создании бэкапов
echo
echo "To create a backup, use:"
echo "pg_dump -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME > backup.sql"
echo
echo "To restore from backup, use:"
echo "psql -h localhost -p $DB_PORT -U $DB_USER -d $DB_NAME < backup.sql"