From 232db222ca62411479bafdd4ea4aea247724ab49 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 17 Sep 2024 03:08:07 +0300 Subject: [PATCH] =?UTF-8?q?=D1=81=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D1=80=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 319 +------------------ handlers/__pycache__/balance.cpython-310.pyc | Bin 0 -> 555 bytes handlers/__pycache__/expiry.cpython-310.pyc | Bin 0 -> 1547 bytes handlers/__pycache__/keys.cpython-310.pyc | Bin 0 -> 1410 bytes handlers/__pycache__/profile.cpython-310.pyc | Bin 0 -> 2173 bytes handlers/__pycache__/start.cpython-310.pyc | Bin 0 -> 1612 bytes handlers/__pycache__/stats.cpython-310.pyc | Bin 0 -> 1907 bytes handlers/balance.py | 10 + handlers/expiry.py | 33 ++ handlers/keys.py | 31 ++ handlers/profile.py | 51 +++ handlers/start.py | 26 ++ handlers/stats.py | 45 +++ key_management.py | 128 ++++++++ 14 files changed, 339 insertions(+), 304 deletions(-) create mode 100644 handlers/__pycache__/balance.cpython-310.pyc create mode 100644 handlers/__pycache__/expiry.cpython-310.pyc create mode 100644 handlers/__pycache__/keys.cpython-310.pyc create mode 100644 handlers/__pycache__/profile.cpython-310.pyc create mode 100644 handlers/__pycache__/start.cpython-310.pyc create mode 100644 handlers/__pycache__/stats.cpython-310.pyc create mode 100644 handlers/balance.py create mode 100644 handlers/expiry.py create mode 100644 handlers/keys.py create mode 100644 handlers/profile.py create mode 100644 handlers/start.py create mode 100644 handlers/stats.py create mode 100644 key_management.py diff --git a/bot.py b/bot.py index ed451561..e284f260 100644 --- a/bot.py +++ b/bot.py @@ -1,314 +1,25 @@ -from aiogram import Bot, Dispatcher, Router, F, types -from aiogram.filters import Command -from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message, ReplyKeyboardMarkup, KeyboardButton +from aiogram import Bot, Dispatcher, Router from aiogram.fsm.storage.memory import MemoryStorage -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from auth import login_with_credentials, link -from client import add_client -from datetime import datetime, timedelta -from config import API_TOKEN, ADMIN_PASSWORD, ADMIN_USERNAME, ADMIN_CHAT_ID, DATABASE_PATH -from database import add_connection, has_active_key, get_active_key_email, get_balance, store_key -import uuid -import aiosqlite -import re - -def escape_markdown(text: str) -> str: - """ - Экранирование специальных символов для Markdown. - """ - escape_chars = r'_*[]()~`>#+-=|{}.!' - return re.sub(r'([%s])' % re.escape(escape_chars), r'\\\1', text) - -class Form(StatesGroup): - waiting_for_key_name = State() - waiting_for_admin_confirmation = State() - waiting_for_statistics = State() - waiting_for_expiry_date = State() - viewing_profile = State() +from config import API_TOKEN +from auth import login_with_credentials bot = Bot(token=API_TOKEN) storage = MemoryStorage() dp = Dispatcher(bot=bot, storage=storage) router = Router() -# Создаем сессию при старте бота -session = login_with_credentials(ADMIN_USERNAME, ADMIN_PASSWORD) - -def create_main_menu_keyboard(): - button_start = KeyboardButton(text="В начало") - return ReplyKeyboardMarkup(keyboard=[[button_start]], resize_keyboard=True) - -@dp.message(Command("start")) -async def start_command(message: Message): - welcome_text = "Добро пожаловать! Вы можете создать ключ для подключения VPN, просмотреть статистику использования, узнать дату окончания ключа или просмотреть ваш профиль." - - button_create_key = InlineKeyboardButton(text='Создать ключ', callback_data='create_key') - button_view_stats = InlineKeyboardButton(text='Посмотреть статистику', callback_data='view_stats') - button_view_expiry = InlineKeyboardButton(text='Дата окончания ключа', callback_data='view_expiry') - button_view_profile = InlineKeyboardButton(text='Мой профиль', callback_data='view_profile') - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_create_key], - [button_view_stats], - [button_view_expiry], - [button_view_profile] - ]) - - main_menu_keyboard = create_main_menu_keyboard() - - await message.reply(welcome_text, reply_markup=keyboard) - await message.answer("Чтобы вернуться в начало, нажмите кнопку ниже:", reply_markup=main_menu_keyboard) +from handlers import start, profile, keys, stats, expiry, balance +from key_management import router as key_management_router -@dp.callback_query(F.data == 'view_profile') -async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - username = callback_query.from_user.username # Получаем никнейм пользователя - - try: - email = await get_active_key_email(tg_id) - if email: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT expiry_time FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: - record = await cursor.fetchone() - if record: - expiry_time = record[0] - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") - else: - expiry_date = "Неизвестно" - - # Получите баланс клиента (замените пример на вашу реальную логику получения баланса) - balance = await get_balance(tg_id) # Предполагается, что у вас есть функция для получения баланса - - profile_message = ( - f"Профиль клиента:\n" - f"Никнейм: @{username}\n" # Добавляем никнейм - f"Email: {email}\n" - f"Дата окончания ключа: {expiry_date}\n" - f"Баланс: {balance}\n" - ) - - # Кнопка для просмотра ключей - button_view_keys = InlineKeyboardButton(text='Мои ключи', callback_data='view_keys') - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [button_view_keys] - ]) - - profile_message += "Вы можете просмотреть ваши ключи ниже:" - else: - profile_message = "У вас нет активных ключей." - keyboard = None - - except Exception as e: - profile_message = f"Ошибка при получении данных профиля: {e}" - keyboard = None - - await callback_query.message.reply(profile_message, reply_markup=keyboard) - await callback_query.answer() +# Регистрация обработчиков +dp.include_router(start.router) +dp.include_router(profile.router) +dp.include_router(keys.router) +dp.include_router(stats.router) +dp.include_router(expiry.router) +dp.include_router(balance.router) +dp.include_router(key_management_router) -@dp.callback_query(F.data == 'view_keys') -async def process_callback_view_keys(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - - try: - async with aiosqlite.connect(DATABASE_PATH) as db: - # Извлекаем ключ для текущего пользователя по его Telegram ID - async with db.execute(''' - SELECT k.key - FROM keys k - JOIN connections c ON k.client_id = c.client_id - WHERE c.tg_id = ? - ''', (tg_id,)) as cursor: - record = await cursor.fetchone() - - if record: - key = record[0] - # Форматируем текст для цитаты - response_message = f"Ваш ключ:\n
{key}
" - else: - response_message = "У вас нет ключей." - - except Exception as e: - response_message = f"Ошибка при получении ключей: {e}" - - # Отправляем сообщение - await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_to_message_id=callback_query.message.message_id) - await callback_query.answer() - - - - -@dp.callback_query(F.data == 'create_key') -async def process_callback_create_key(callback_query: types.CallbackQuery, state: FSMContext): - await callback_query.message.reply("Введите имя вашего профиля VPN:") - await state.set_state(Form.waiting_for_key_name) - await callback_query.answer() - -@dp.message() -async def handle_text(message: types.Message, state: FSMContext): - current_state = await state.get_state() - - if message.text == "В начало": - await start_command(message) - return - - if current_state == Form.waiting_for_key_name.state: - tg_id = message.from_user.id - key_name = message.text - tg_name = message.from_user.username - - if not key_name: - await message.reply("Имя клиента не указано.") - await state.clear() - return - - if await has_active_key(tg_id): - await message.reply("У вас уже есть активный ключ. Вы не можете создать новый.") - await state.clear() - return - - # Сохраняем информацию о запросе в контексте состояния - await state.update_data(key_name=key_name, tg_id=tg_id) - - # Отправляем запрос на подтверждение админу - admin_message = ( - f"Новый запрос на создание ключа:\n" - f"Телеграм-ID: {tg_id}\n" - f"Имя ключа: {key_name}\n" - f"Имя пользователя Telegram: @{tg_name}\n" - f"Введите 'yes' для подтверждения или 'no' для отклонения." - ) - keyboard = InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text='Да', callback_data=f'confirm_key_{tg_id}_{key_name}')], - [InlineKeyboardButton(text='Нет', callback_data=f'reject_key_{tg_id}')] - ]) - await bot.send_message(ADMIN_CHAT_ID, admin_message, reply_markup=keyboard) - - await state.set_state(Form.waiting_for_admin_confirmation) - await message.reply("Ожидайте подтверждения администратора.") - await state.clear() - -@dp.callback_query(F.data.startswith('confirm_key_')) -async def handle_admin_confirmation(callback_query: CallbackQuery, state: FSMContext): - if callback_query.message.chat.id != int(ADMIN_CHAT_ID): - return - - data = callback_query.data.split('_', 3) - tg_id = int(data[2]) - key_name = data[3] - - try: - client_id = str(uuid.uuid4()) - email = key_name - limit_ip = 1 - total_gb = 0 - current_time = datetime.utcnow() - expiry_time = int((current_time + timedelta(days=30)).timestamp() * 1000) - enable = True - flow = "xtls-rprx-vision" - balance = 0 - - add_client(session, client_id, email, tg_id, limit_ip, total_gb, expiry_time, enable, flow) - - connection_link = link(session, client_id, email) - - await add_connection(tg_id, client_id, email, expiry_time, balance) - await store_key(client_id, email, connection_link) - await bot.send_message(tg_id, f"Ключ создан:\n
{connection_link}
", parse_mode="HTML") - await callback_query.message.reply("Ключ создан и отправлен клиенту.") - except Exception as e: - await callback_query.message.reply(f"Ошибка при создании ключа: {e}") - - await callback_query.answer() - -@dp.callback_query(F.data.startswith('reject_key_')) -async def handle_rejection(callback_query: CallbackQuery, state: FSMContext): - if callback_query.message.chat.id != int(ADMIN_CHAT_ID): - return - - tg_id = int(callback_query.data.split('_', 3)[2]) - await bot.send_message(tg_id, "Создание ключа отклонено.") - await callback_query.message.reply("Запрос отклонен.") - await callback_query.answer() - -@dp.callback_query(F.data == 'view_stats') -async def process_callback_view_stats(callback_query: types.CallbackQuery, state: FSMContext): - tg_id = callback_query.from_user.id - - try: - email = await get_active_key_email(tg_id) - if email: - # Получаем client_id из базы данных - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT client_id FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: - record = await cursor.fetchone() - if record: - client_id = record[0] - # Передаем оба параметра в функцию link - connection_link = link(session, client_id, email) - - # Извлечение данных о загрузке и выгрузке из ссылки - up_match = re.search(r'up=(\d+)', connection_link) - down_match = re.search(r'down=(\d+)', connection_link) - - up = up_match.group(1) if up_match else "Неизвестно" - down = down_match.group(1) if down_match else "Неизвестно" - - statistics = f"Статистика вашего ключа:\nЗагрузка: {up} MB\nВыгрузка: {down} MB" - else: - statistics = "У вас нет активных ключей." - else: - statistics = "У вас нет активных ключей." - - except Exception as e: - statistics = f"Ошибка при получении статистики: {e}" - - await callback_query.message.reply(f"Ваша статистика:\n{statistics}") - await callback_query.answer() - - -@dp.callback_query(F.data == 'view_expiry') -async def process_callback_view_expiry(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - - try: - email = await get_active_key_email(tg_id) - if email: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT expiry_time FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: - record = await cursor.fetchone() - if record: - expiry_time = record[0] - expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") - message_text = f"Дата окончания вашего ключа: {expiry_date}" - else: - message_text = "У вас нет активных ключей." - else: - message_text = "У вас нет активных ключей." - - except Exception as e: - message_text = f"Ошибка при получении даты окончания ключа: {e}" - - await callback_query.message.reply(message_text) - await callback_query.answer() - -@dp.callback_query(F.data == 'view_balance') -async def process_callback_view_balance(callback_query: types.CallbackQuery): - tg_id = callback_query.from_user.id - - try: - async with aiosqlite.connect(DATABASE_PATH) as db: - async with db.execute("SELECT balance FROM connections WHERE tg_id = ? AND expiry_time > ?", - (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: - record = await cursor.fetchone() - balance = record[0] - message_text = f"Ваш баланс: {balance} RUB" - - except Exception as e: - message_text = f"Ошибка при получении баланса: {e}" - - await callback_query.message.reply(message_text) - await callback_query.answer() +async def on_startup(dispatcher: Dispatcher): + await login_with_credentials() diff --git a/handlers/__pycache__/balance.cpython-310.pyc b/handlers/__pycache__/balance.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3100575b4b5ac805eb908d3f52c4bf73a76b1b54 GIT binary patch literal 555 zcmY+B!Ab)$5Qa19cDt2UJbCj07QuZ15uu(7>TNGex=GuuSyPj3rJ$g+528n(qi>O` z7kvdmaAsRd9hk{ZNap|ktm$?S7{>4VYY4#DCpG&KU~`G-s7NM3vl3D$dCGx$t{Z7X z@g++G6}&ZNLk27M4zj6W5)QcxWos2IA#F*1%i`!8r?8m&X0@0q=ffK_tCa2MD7_2% z=9oQ9M|4kYg2Hz+fcJf8UZ}@xAhfv8RSZ6mq7t}5{=sYukN=z|V^gYRu7yml)I7O0 z+GGP$C1Y_fwX!bR-aeZyd~~iwIgsKaBstPOV785mKg@;J1Cifl4>M&K{}|~Uajr4l zA(mGl$_YG!WXK79G}#_nQ)V-#tmlP{xg|4beUuflCf$^Xc2B0(&2Jk;6rUOIu6O+r(kLoIaM+6xiPTz1z#%FrAu)uHrjSI6Y6-12JL6=NU9UU4 zj^k*J(z*zwN+2OlAdoFi+`>ns&?@br|G@e>N4S*#pyCqVtli*Bh*`azdGp@;y*Ka8 zyr5XjAs9~{J+A$sBJ`(Z#)ksTTR<$j4RelPbZICtk)^s!F=fc&e{f)pV_R zT2%wN>ScYss$;}R7gY^r^m0_A**mC@sm}0DzDB4)b9a<3t`?}WhGz1AfhROWgc`LQ zEEL*m(2N+L!D4!gMV9SE?jEytS=(a1?Rv1UQ#)dj>$4SLic1UY3vVy1RjiK|)|VYD z!A3)_3D6=?D32YZ9XQc00k335Vwf~+iZwEdr?|C*Y zFR{JyTHpoNW)PLP?K<@s56eU2%Z;|suXwh=h!y8rM*8GjbGDBgm!f2l;x((+vC+FWU^`8w<;-y}za+sTocJV@>Voen-vj*`>lWbl=l`~tEs zlH=spiNAfYuQryjI~A#)r?w%-tXjYZCOP-kKigc5&D z7Dl`#Sz%Q69oCSQ5LvJc?JXu$&Kh1@Xm&kpK_kfVqI99AB@$}d1q7j+LUWot47kuZ za{^99eyq;tjO-PnFoL!swjT-1()T_5A)ATMrNP9az+l7ymB}}E@J(VoK0n>Fn<|LXNn>V>D=PB&t-GqaG_Az z$m~rUvMv*zW|B`zfd=`mxm&|@Ydmd;!z>gRN8RLO*-TRGvBSt-!qXG!SNwoBJvJ|I RZ74w}Q30aV(?|;t-#@%i))W8$ literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/keys.cpython-310.pyc b/handlers/__pycache__/keys.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9876cae941c05a1b03de4de2c69ad1d236630b03 GIT binary patch literal 1410 zcmaJ>%WoS+7@wJa`bpKI@DM@^ry8Lcq;jc3AW4l<)H;wvO6$vNvolU?@57y4$91$u zDXG+6;J}fp$PuUB3R0y4DjfO`tgmzJg^-PapBMvUyE zhQW-HNj0k9L%~t z_Yltk_MfxW?a*h{9nYcF3v8#l7J8u#CaT*`Kt0BzsuZ2=4um=9IsPVf<_&2`TFfI} z04B<=<9VBo+p(|p7#|$-&dhKKJ(fbRgReKR^skMFc=t+7MoO$6;B)9ABy$;kzc?fb z8EF(JxQP$Q(fK<=B~eED!4x-C6P16EsFb{ch8k58?GB>qU1g{Tgla>51GK3y_mzoW zjDo_4Q`u;-k1>+-Dy;uSZX8Djuc;_D1~EoM1Ebvn)f3|mx=Wbv*y)=OipcB!_sZgpm%3f76+&sJA1 zTe3b@=fvITD;MjQ8wLS$<5n0%mTRrlL6PgVSPwA5tdHaMtw(b{yJ1iy~)>f%}z`$TfNB+r5Z!L0VMgoK_fJ^I9_PQ|G zb>>3z3A4#!cRLK2$knd9tSghqH3=z3ETD(lg)ucF^3UxF-3g*T;}w$^rG%CxQ4qQ* zbhpQ&kPDqNH{?_(P(E>rv#1L-f)FM?3(T_ioDkX6;>|7(UAVQ6n(e5uvxa;hKxEG( zguIG%tZK!7iB`rcF>x8|b_2UY2LY+gQ?s0`#3GI0^6 Jj*Dal{|5mUe!Bnw literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/profile.cpython-310.pyc b/handlers/__pycache__/profile.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..697ac76fc72f185bdc859791be8a823d0a4eb587 GIT binary patch literal 2173 zcmaJDOK%%RcxSv`@2($l9#kp?X)loos0m0QB&)Pdlav%l3r(b|7Lv90j8iB3aA(&^ z9POf{DWXy-6^K(yivuTar6LfTCOz~Itgmx~OZf>PX!vIBq$vk>HM8@5^S$PK&AQpF zF2VEL<0n;3lce7TaPUb2a1w@h4JINHWJxwEA{!Sm5-_$Bwp^6$WHA}T2}`k4#gwfU z)fkp7&DM%qyiQtrQ3oEyO52%YCdQ?#teq?7kVNsCRP3QWtvpeQc2!zOM5k!3m!xT? zDw7P!UX|BSv5zFDrJ>v(V2w0{dD3szsmGP`Zo{Vx5Cdl%tLD)6X|wE_jGSutzU%xK zJ7u!@MqN-coZ z2Piu}Ix~7|bb6xn;pog{1qrLVk62?EV=z47<3O4N_swH4AD`u!g&JKf)tOtZSwe{n zN=*2o$`2CD0r_9Aja+hVIT@mwUM0FZJ}y$(4tAg!S) zUdQ{CgP{^At;~9tGfV|3_Fj-8iNjJTIJ1FD@Da#?R*uN)DAXKGlA(41w9D|WB{~|B zWMKpyVDaNOpMJKbg5or z&63DavHel z#iN7v(LrJiP8J5I3WL*)0l-IhqK#-X`X#y_ZM0Y0SEB7`r}4Vj__nX9r!i@w+)zIqlX3Ks1s<3TF#7?B+yvI`_VxCs&kP3V3^&FF&s`xi`Vm|K9ub=1SdYM(t@axD z0o)3^fch!+QCCXGz)l2i77S=18Aj_?vt)Og+##-2nQNCC9%UTY2v^0*F>T5e(8)Ek z=6aW{nooJEn>b7~1D>Kwv;tj~Cu)w*RDn_&e#LPYxh8TA`iNcUYL)txORhtC9yW!h z1ACs&ssdyw;AXOz#C^iGiKPmyiv;B<5agM&lqX}WxnerrB6RW|CSuF_1bH$}Uv?y3 zcUm+iWv)~j%ySu67_GRB@brN~*at-K=BaKo=RMt)S=!T@FUJ=|X+LAQx|b0g(YO(J zCXs;rWniy>y`}wZI@rbI);j}qctyo3!Z?RyG=LOz2xWFL5|9dO1;6^|E4w)zV~pfo zjOC{|A^(GAgOp4*DNz_CK@<{!j*Wo ziyDsa!UJv=7nfN!A>1O;ogj!fvX=x~{~lY_vxgz_qD~n60zjAns@7&ZX`rz9YK_Ce xvt!-M?rk^?UIH?kBhG2i58NV@^G3Acpw9>f;0F<^e+sc#LNHy literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/start.cpython-310.pyc b/handlers/__pycache__/start.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88c6e1e8e29591442a14e6d39ce13e3bef44cb46 GIT binary patch literal 1612 zcma)6&2Jk;6yI5|cl{ZsNhv)Qb3w9H#rFsyN^@w05}`KYu$R@wGjZJPdac>rq(+XE z)|HAx0#ei?67+<)g{XnhG^qst!0dIdT;N|o9Qe)HZcSAXYt8u0Z{GKN^P+0CWZ+x; z<@b$6+c17phw0D4;X|x=6B{)s35<}qBs5)~Z&3Zw9!a4?L zCg+hTv{!E|tt_v4H*eio^Ol!nK2Cg|NGD`*?6+BqD6hs}0sncdU$NptZ1)KzyCgwM zXpUM3=B^1kPi;--KqEv=TcC@yr0IM@24Ny ztwEuCK%CUG`#Ah0o{l)`;+Z(hzRVugft+Pujnq7ypyEWF zi&Ma#ezJNUcghkJKp)ia324mrp%IE<4~&XB70_!{YKK75A1BZbXB-|LSCpJRJx`VeTPD-&O-AXqs)6K%TN^?6-JL7|HBL#1I0w|Jx9@1 zi=k}ikbW_01e6kYCyNc6t5?M_iwy3lO+7T3*T$vocjFz*fdVfpw6ep3RunQ%*X7c> zo&esflF145D(P}=%r%uzIX{h!v&ZL^g}Ko0biI&u)0a8?-r(18^=G5G3DeE(z^Bb6 zw%xoF1(CNNCCyEYr-1Resi((6uP>{b^5Ci(T`HhhsWA5}vtm|=`kBNU{|bLyaW0!z z39o}?wJ|FlzZ12&A951_&ta3bF|>d>V=l&AjcHk(pn7P&F^_gD>TYz}yr}qXuBYlK pm(I#)$Z4}W!l~^Y#a?(njA$CL57ZcpXRys06>=5M+X&+d`5VOmifaG> literal 0 HcmV?d00001 diff --git a/handlers/__pycache__/stats.cpython-310.pyc b/handlers/__pycache__/stats.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04fb78cce7153df0d11cde16e66c298b9ec3150d GIT binary patch literal 1907 zcmaJ?Pi)gx7=Q1@c5F9EO9>s*HqDnwWRs|nc0y1HG*gk*!rCH4w8?Vq*VNR1!n4!R z&Y22f+9alFLKBmQm=f)@TQ^j#V*(D-?#}gHcY))syRh#$0g5!u(fjxL`}6zW_w$#q zSkw_b^v@@&@2CjlHRnmbT$eMM zp7+(dnw@1&^R>D*(%0)c=quisU#J%{SswPqdeJY{OBhkoMfGv2(jqN&NqvHj_YOF! zquoS(?C2CLa)ZXEWykIY zHP`8;Nqx)pKvVMEV4Y8SVZ#l~wi~aRHlq#=V%PE_uu~l?rm^c&*cWH3%hfa0rJ8x6 zx;zKQrRwbbxdrp$Qf+aeI$wK!zEE9Sx^#YV*2V(P=n;DY<1CCwTy=u3z%8x|++>04 z8!l~|QEbH`ssvFjSh4*RVkJ=ir!u`3`gD53vz+N!x-q>JdZD=z#?xz7;CPfp)0yqj z<|bE9c$UB7STn*f5;1keCSc(u+w!~>%U(CHwkX?t1)oqBu>OD%eFbQ*V}bAr?%=I& zW0K%5J_mK4LmiS3b|=pDq*(6b_t8-LJ?rF>Tu*Uug5!Lw_Ow3g>3!fu8Fle0>5~_l za~&y>dSm^OWk*hA<|eX}dmVL@xDXc;B_Z8hCm)x3sX-_a9|eUfPVBx8=6=y#E$C1Wt__vB6?0Jd}rmmmiTe%;&BIP z{4T@zU(Ij!Yd-fJye6Uez~&A{ogzkCz|Ew17u_Qw>Sc)ADSlQA{Tg{V4K6c)#Z0Sze4u<=u;#I2* zQ+hAmP507Y(_hlv!S%t7^kMp_by~dW!(j);UMAj4AEY}5XzdKX1dIFWBLgJA5569J zk?tJV)9=!q^!{Lb&;?hZdE97CffHl?jGo>e+MV#$~bWyCb}ZmEH{j5Ks@IL zG0O{)(u(aMY;#Q%1U^~6$<V{{oG{9jh+52E<8p(AttQuM*KFDp z#pZc1kE{meGNVmzlPgvbwJEEV*a6{1&RD?Ztj}@ctZ>C{u_$C*Vbl(p!?hPg;^Qw# zWM=&ksA2jRpyC-dMtcGXBtaFfi)Q0Sv1>;h)2u@cr*RScjV23iKxe*aH!qu8){|yH zi)^cekP?<~87p`S7oHI;PAV?o0x950GKtGXA@BZu ?", + (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: + record = await cursor.fetchone() + if record: + expiry_time = record[0] + expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") + message_text = f"Дата окончания вашего ключа: {expiry_date}" + else: + message_text = "У вас нет активных ключей." + else: + message_text = "У вас нет активных ключей." + + except Exception as e: + message_text = f"Ошибка при получении даты окончания ключа: {e}" + + await callback_query.message.reply(message_text) + await callback_query.answer() diff --git a/handlers/keys.py b/handlers/keys.py new file mode 100644 index 00000000..043b4fc7 --- /dev/null +++ b/handlers/keys.py @@ -0,0 +1,31 @@ +from aiogram import types, Router +import aiosqlite +from config import DATABASE_PATH +from bot import bot +router = Router() + +@router.callback_query(lambda c: c.data == 'view_keys') +async def process_callback_view_keys(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + + try: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute(''' + SELECT k.key + FROM keys k + JOIN connections c ON k.client_id = c.client_id + WHERE c.tg_id = ? + ''', (tg_id,)) as cursor: + record = await cursor.fetchone() + + if record: + key = record[0] + response_message = f"Ваш ключ:\n
{key}
" + else: + response_message = "У вас нет ключей." + + except Exception as e: + response_message = f"Ошибка при получении ключей: {e}" + + await bot.send_message(tg_id, response_message, parse_mode="HTML", reply_to_message_id=callback_query.message.message_id) + await callback_query.answer() diff --git a/handlers/profile.py b/handlers/profile.py new file mode 100644 index 00000000..ec859c61 --- /dev/null +++ b/handlers/profile.py @@ -0,0 +1,51 @@ +from aiogram import types, Router +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup +from database import get_active_key_email, get_balance +from datetime import datetime +import aiosqlite +from aiogram.fsm.context import FSMContext +from config import DATABASE_PATH + +router = Router() + +@router.callback_query(lambda c: c.data == 'view_profile') +async def process_callback_view_profile(callback_query: types.CallbackQuery, state: FSMContext): + tg_id = callback_query.from_user.id + username = callback_query.from_user.username + + try: + email = await get_active_key_email(tg_id) + if email: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT expiry_time FROM connections WHERE tg_id = ? AND expiry_time > ?", + (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: + record = await cursor.fetchone() + if record: + expiry_time = record[0] + expiry_date = datetime.utcfromtimestamp(expiry_time / 1000).strftime("%Y-%m-%d %H:%M:%S") + else: + expiry_date = "Неизвестно" + + balance = await get_balance(tg_id) + profile_message = ( + f"Профиль клиента:\n" + f"Никнейм: @{username}\n" + f"Email: {email}\n" + f"Дата окончания ключа: {expiry_date}\n" + f"Баланс: {balance}\n" + ) + + button_view_keys = InlineKeyboardButton(text='Мои ключи', callback_data='view_keys') + keyboard = InlineKeyboardMarkup(inline_keyboard=[[button_view_keys]]) + + profile_message += "Вы можете просмотреть ваши ключи ниже:" + else: + profile_message = "У вас нет активных ключей." + keyboard = None + + except Exception as e: + profile_message = f"Ошибка при получении данных профиля: {e}" + keyboard = None + + await callback_query.message.reply(profile_message, reply_markup=keyboard) + await callback_query.answer() diff --git a/handlers/start.py b/handlers/start.py new file mode 100644 index 00000000..b245e72e --- /dev/null +++ b/handlers/start.py @@ -0,0 +1,26 @@ +from aiogram import types, Router +from aiogram.filters import Command +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, KeyboardButton +from config import ADMIN_CHAT_ID + +router = Router() + +@router.message(Command("start")) +async def start_command(message: types.Message): + welcome_text = "Добро пожаловать! Вы можете создать ключ для подключения VPN, просмотреть статистику использования, узнать дату окончания ключа или просмотреть ваш профиль." + + button_create_key = InlineKeyboardButton(text='Создать ключ', callback_data='create_key') + button_view_stats = InlineKeyboardButton(text='Посмотреть статистику', callback_data='view_stats') + button_view_expiry = InlineKeyboardButton(text='Дата окончания ключа', callback_data='view_expiry') + button_view_profile = InlineKeyboardButton(text='Мой профиль', callback_data='view_profile') + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [button_create_key], + [button_view_stats], + [button_view_expiry], + [button_view_profile] + ]) + + main_menu_keyboard = ReplyKeyboardMarkup(keyboard=[[KeyboardButton(text="В начало")]], resize_keyboard=True) + + await message.reply(welcome_text, reply_markup=keyboard) + await message.answer("Чтобы вернуться в начало, нажмите кнопку ниже:", reply_markup=main_menu_keyboard) diff --git a/handlers/stats.py b/handlers/stats.py new file mode 100644 index 00000000..c0b1ae12 --- /dev/null +++ b/handlers/stats.py @@ -0,0 +1,45 @@ +from aiogram import types, Router +import re +from database import get_active_key_email +from auth import link, login_with_credentials +from datetime import datetime +import aiosqlite +from config import DATABASE_PATH +from config import ADMIN_USERNAME, ADMIN_PASSWORD + + +router = Router() +session = login_with_credentials(ADMIN_USERNAME, ADMIN_PASSWORD) + +@router.callback_query(lambda c: c.data == 'view_stats') +async def process_callback_view_stats(callback_query: types.CallbackQuery): + tg_id = callback_query.from_user.id + + try: + email = await get_active_key_email(tg_id) + if email: + async with aiosqlite.connect(DATABASE_PATH) as db: + async with db.execute("SELECT client_id FROM connections WHERE tg_id = ? AND expiry_time > ?", + (tg_id, int(datetime.utcnow().timestamp() * 1000))) as cursor: + record = await cursor.fetchone() + if record: + client_id = record[0] + connection_link = link(session, client_id, email) + + up_match = re.search(r'up=(\d+)', connection_link) + down_match = re.search(r'down=(\d+)', connection_link) + + up = up_match.group(1) if up_match else "Неизвестно" + down = down_match.group(1) if down_match else "Неизвестно" + + statistics = f"Статистика вашего ключа:\nЗагрузка: {up} MB\nВыгрузка: {down} MB" + else: + statistics = "У вас нет активных ключей." + else: + statistics = "У вас нет активных ключей." + + except Exception as e: + statistics = f"Ошибка при получении статистики: {e}" + + await callback_query.message.reply(f"Ваша статистика:\n{statistics}") + await callback_query.answer() diff --git a/key_management.py b/key_management.py new file mode 100644 index 00000000..e99727aa --- /dev/null +++ b/key_management.py @@ -0,0 +1,128 @@ +from aiogram import Bot, Dispatcher, Router, F, types +from aiogram.filters import Command +from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, Message, ReplyKeyboardMarkup, KeyboardButton +from aiogram.fsm.storage.memory import MemoryStorage +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from auth import login_with_credentials, link +from client import add_client +from datetime import datetime, timedelta +from config import API_TOKEN, ADMIN_PASSWORD, ADMIN_USERNAME, ADMIN_CHAT_ID, DATABASE_PATH +from database import add_connection, has_active_key, get_active_key_email, get_balance, store_key +import uuid +import aiosqlite +import re +from bot import dp +from handlers.start import start_command +from bot import bot +from handlers.stats import session + +router = Router() + +def escape_markdown(text: str) -> str: + """ + Экранирование специальных символов для Markdown. + """ + escape_chars = r'_*[]()~`>#+-=|{}.!' + return re.sub(r'([%s])' % re.escape(escape_chars), r'\\\1', text) + +class Form(StatesGroup): + waiting_for_key_name = State() + waiting_for_admin_confirmation = State() + waiting_for_statistics = State() + waiting_for_expiry_date = State() + viewing_profile = State() + + +@dp.callback_query(F.data == 'create_key') +async def process_callback_create_key(callback_query: types.CallbackQuery, state: FSMContext): + await callback_query.message.reply("Введите имя вашего профиля VPN:") + await state.set_state(Form.waiting_for_key_name) + await callback_query.answer() + +@dp.message() +async def handle_text(message: types.Message, state: FSMContext): + current_state = await state.get_state() + + if message.text == "В начало": + await start_command(message) + return + + if current_state == Form.waiting_for_key_name.state: + tg_id = message.from_user.id + key_name = message.text + tg_name = message.from_user.username + + if not key_name: + await message.reply("Имя клиента не указано.") + await state.clear() + return + + if await has_active_key(tg_id): + await message.reply("У вас уже есть активный ключ. Вы не можете создать новый.") + await state.clear() + return + + # Сохраняем информацию о запросе в контексте состояния + await state.update_data(key_name=key_name, tg_id=tg_id) + + # Отправляем запрос на подтверждение админу + admin_message = ( + f"Новый запрос на создание ключа:\n" + f"Телеграм-ID: {tg_id}\n" + f"Имя ключа: {key_name}\n" + f"Имя пользователя Telegram: @{tg_name}\n" + f"Введите 'yes' для подтверждения или 'no' для отклонения." + ) + keyboard = InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Да', callback_data=f'confirm_key_{tg_id}_{key_name}')], + [InlineKeyboardButton(text='Нет', callback_data=f'reject_key_{tg_id}')] + ]) + await bot.send_message(ADMIN_CHAT_ID, admin_message, reply_markup=keyboard) + + await state.set_state(Form.waiting_for_admin_confirmation) + await message.reply("Ожидайте подтверждения администратора.") + await state.clear() + +@dp.callback_query(F.data.startswith('confirm_key_')) +async def handle_admin_confirmation(callback_query: CallbackQuery, state: FSMContext): + if callback_query.message.chat.id != int(ADMIN_CHAT_ID): + return + + data = callback_query.data.split('_', 3) + tg_id = int(data[2]) + key_name = data[3] + + try: + client_id = str(uuid.uuid4()) + email = key_name + limit_ip = 1 + total_gb = 0 + current_time = datetime.utcnow() + expiry_time = int((current_time + timedelta(days=30)).timestamp() * 1000) + enable = True + flow = "xtls-rprx-vision" + balance = 0 + + add_client(session, client_id, email, tg_id, limit_ip, total_gb, expiry_time, enable, flow) + + connection_link = link(session, client_id, email) + + await add_connection(tg_id, client_id, email, expiry_time, balance) + await store_key(client_id, email, connection_link) + await bot.send_message(tg_id, f"Ключ создан:\n
{connection_link}
", parse_mode="HTML") + await callback_query.message.reply("Ключ создан и отправлен клиенту.") + except Exception as e: + await callback_query.message.reply(f"Ошибка при создании ключа: {e}") + + await callback_query.answer() + +@dp.callback_query(F.data.startswith('reject_key_')) +async def handle_rejection(callback_query: CallbackQuery, state: FSMContext): + if callback_query.message.chat.id != int(ADMIN_CHAT_ID): + return + + tg_id = int(callback_query.data.split('_', 3)[2]) + await bot.send_message(tg_id, "Создание ключа отклонено.") + await callback_query.message.reply("Запрос отклонен.") + await callback_query.answer()