diff --git a/app/marznode/operations.py b/app/marznode/operations.py index 815cf4c..5060903 100644 --- a/app/marznode/operations.py +++ b/app/marznode/operations.py @@ -1,9 +1,9 @@ from collections import defaultdict from typing import TYPE_CHECKING -from .grpclib import MarzNodeGRPCLIB -from .grpcio import MarzNodeGRPCIO from app import marznode +from .grpcio import MarzNodeGRPCIO +from .grpclib import MarzNodeGRPCLIB from ..models.node import NodeConnectionBackend from ..models.user import UserStatus diff --git a/app/telegram/__init__.py b/app/telegram/__init__.py index 5594627..5746191 100644 --- a/app/telegram/__init__.py +++ b/app/telegram/__init__.py @@ -1,42 +1,28 @@ -import importlib.util -from os.path import dirname -from config import TELEGRAM_API_TOKEN, TELEGRAM_PROXY_URL -from telebot.async_telebot import AsyncTeleBot from telebot import asyncio_helper +from telebot.async_telebot import AsyncTeleBot + +from config import TELEGRAM_API_TOKEN, TELEGRAM_PROXY_URL +from app.telegram.report import ( + report, + report_new_user, + report_user_modification, + report_user_deletion, + report_status_change, + report_user_usage_reset, + report_user_subscription_revoked, +) bot = None if TELEGRAM_API_TOKEN: asyncio_helper.proxy = TELEGRAM_PROXY_URL bot = AsyncTeleBot(TELEGRAM_API_TOKEN) -handler_names = ["admin", "user"] - async def start_bot(): if bot: - handler_dir = dirname(__file__) + "/handlers/" - for name in handler_names: - spec = importlib.util.spec_from_file_location( - name, f"{handler_dir}{name}.py" - ) - spec.loader.exec_module(importlib.util.module_from_spec(spec)) - - from app.telegram import utils # setup custom handlers - - utils.setup() await bot.polling() -from .handlers.report import ( # noqa - report, - report_new_user, - report_user_modification, - report_user_deletion, - report_status_change, - report_user_usage_reset, - report_user_subscription_revoked, -) - __all__ = [ "bot", "report", diff --git a/app/telegram/handlers/admin.py b/app/telegram/handlers/admin.py deleted file mode 100644 index 13e070d..0000000 --- a/app/telegram/handlers/admin.py +++ /dev/null @@ -1,1674 +0,0 @@ -import io -import math -import re -import random -import string -import os -from datetime import datetime - -import qrcode -import sqlalchemy -from dateutil.relativedelta import relativedelta -from telebot import types -from telebot.util import user_link -from telebot.asyncio_handler_backends import State, StatesGroup - -from app.db import GetDB, crud -from app.models.user import ( - UserCreate, - UserModify, - UserResponse, - UserStatus, - UserStatusModify, -) -from app.models.proxy import ProxyTypes -from app.telegram import bot -from app.telegram.utils.custom_filters import query_equals, query_startswith -from app.telegram.utils.keyboard import BotKeyboard -from app.utils.store import MemoryStorage -from app.utils.system import cpu_usage, memory_usage, readable_size - -try: - from app.utils.system import realtime_bandwith as realtime_bandwidth -except ImportError: - from app.utils.system import realtime_bandwidth - -from config import TELEGRAM_LOGGER_CHANNEL_ID, TELEGRAM_DEFAULT_VLESS_FLOW - -mem_store = MemoryStorage() - - -class botStates(StatesGroup): - start = State() - add_data = State() # statesgroup should contain states - add_time = State() - edit_user_data_limit = State() - edit_user_expire = State() - edit_note = State() - # add_user_from_template = State() - add_user_username = State() - add_user_data_limit = State() - add_user_expire = State() - - -def get_system_info(): - mem = memory_usage() - cpu = cpu_usage() - with GetDB() as db: - bandwidth = crud.get_system_usage(db) - total_users = crud.get_users_count(db) - users_active = crud.get_users_count(db, UserStatus.active) - return """\ -πŸŽ› *CPU Cores*: `{cpu_cores}` -πŸ–₯ *CPU Usage*: `{cpu_percent}%` -βž–βž–βž–βž–βž–βž–βž– -πŸ“Š *Total Memory*: `{total_memory}` -πŸ“ˆ *In Use Memory*: `{used_memory}` -πŸ“‰ *Free Memory*: `{free_memory}` -βž–βž–βž–βž–βž–βž–βž– -⬇️ *Download Usage*: `{down_bandwidth}` -⬆️ *Upload Usage*: `{up_bandwidth}` -↕️ *Total Usage*: `{total_bandwidth}` -βž–βž–βž–βž–βž–βž–βž– -πŸ‘₯ *Total Users*: `{total_users}` -🟒 *Active Users*: `{active_users}` -πŸ”΄ *Deactivate Users*: `{deactivate_users}` -βž–βž–βž–βž–βž–βž–βž– -⏫ *Upload Speed*: `{up_speed}` -⏬ *Download Speed*: `{down_speed}` -""".format( - cpu_cores=cpu.cores, - cpu_percent=cpu.percent, - total_memory=readable_size(mem.total), - used_memory=readable_size(mem.used), - free_memory=readable_size(mem.free), - total_bandwidth=readable_size(bandwidth.uplink + bandwidth.downlink), - up_bandwidth=readable_size(bandwidth.uplink), - down_bandwidth=readable_size(bandwidth.downlink), - total_users=total_users, - active_users=users_active, - deactivate_users=total_users - users_active, - up_speed=readable_size(realtime_bandwidth().outgoing_bytes), - down_speed=readable_size(realtime_bandwidth().incoming_bytes), - ) - - -def schedule_delete_message(chat_id, *message_ids: int) -> None: - messages: list[int] = mem_store.get(f"{chat_id}:messages_to_delete", []) - for mid in message_ids: - messages.append(mid) - mem_store.set(f"{chat_id}:messages_to_delete", messages) - - -async def cleanup_messages(chat_id: int) -> None: - messages: list[int] = mem_store.get(f"{chat_id}:messages_to_delete", []) - for message_id in messages: - try: - await bot.delete_message(chat_id, message_id) - except: - pass - mem_store.set(f"{chat_id}:messages_to_delete", []) - - -@bot.message_handler(commands=["start", "help"], is_admin=True) -async def help_command(message: types.Message): - await cleanup_messages(message.chat.id) - # bot.clear_step_handler_by_chat_id(message.chat.id) - await bot.delete_state(message.from_user.id, message.chat.id) - return await bot.reply_to( - message, - """ -{user_link} Welcome to Marzban Telegram-Bot Admin Panel. -Here you can manage your users and proxies. -To get started, use the buttons below. -""".format( - user_link=user_link(message.from_user) - ), - parse_mode="html", - reply_markup=BotKeyboard.main_menu(), - ) - - -@bot.callback_query_handler(query_equals("system"), is_admin=True) -async def system_command(call: types.CallbackQuery): - return await bot.edit_message_text( - get_system_info(), - call.message.chat.id, - call.message.message_id, - parse_mode="MarkdownV2", - reply_markup=BotKeyboard.main_menu(), - ) - - -@bot.callback_query_handler(query_equals("restart"), is_admin=True) -async def restart_command(call: types.CallbackQuery): - await bot.edit_message_text( - "⚠️ Are you sure? This will restart Xray core.", - call.message.chat.id, - call.message.message_id, - reply_markup=BotKeyboard.confirm_action(action="do_restart"), - ) - - -@bot.callback_query_handler(query_startswith("delete:"), is_admin=True) -async def delete_user_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - await bot.edit_message_text( - f"⚠️ Are you sure? This will delete user `{username}`.", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_delete", username=username), - ) - - -@bot.callback_query_handler(query_startswith("suspend:"), is_admin=True) -async def suspend_user_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - await bot.edit_message_text( - f"⚠️ Are you sure? This will suspend user `{username}`.", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_suspend", username=username), - ) - - -@bot.callback_query_handler(query_startswith("activate:"), is_admin=True) -async def activate_user_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - await bot.edit_message_text( - f"⚠️ Are you sure? This will activate user `{username}`.", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action( - action="do_activate", username=username - ), - ) - - -@bot.callback_query_handler(query_startswith("reset_usage:"), is_admin=True) -async def reset_usage_user_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - await bot.edit_message_text( - f"⚠️ Are you sure? This will Reset Usage of user `{username}`.", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action( - action="do_reset_usage", username=username - ), - ) - - -@bot.callback_query_handler(query_equals("edit_all"), is_admin=True) -async def edit_all_command(call: types.CallbackQuery): - with GetDB() as db: - total_users = crud.get_users_count(db) - active_users = crud.get_users_count(db, UserStatus.active) - disabled_users = crud.get_users_count(db, UserStatus.disabled) - expired_users = crud.get_users_count(db, UserStatus.expired) - limited_users = crud.get_users_count(db, UserStatus.limited) - text = f""" -πŸ‘₯ *Total Users*: `{total_users}` -βœ… *Active Users*: `{active_users}` -❌ *Disabled Users*: `{disabled_users}` -πŸ•° *Expired Users*: `{expired_users}` -πŸͺ« *Limited Users*: `{limited_users}`""" - return await bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.edit_all_menu(), - ) - - -@bot.callback_query_handler(query_equals("delete_expired"), is_admin=True) -async def delete_expired_command(call: types.CallbackQuery): - await bot.edit_message_text( - f"⚠️ Are you sure? This will *DELETE All Expired Users*‼️", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_delete_expired"), - ) - - -@bot.callback_query_handler(query_equals("delete_limited"), is_admin=True) -async def delete_limited_command(call: types.CallbackQuery): - await bot.edit_message_text( - f"⚠️ Are you sure? This will *DELETE All Limited Users*‼️", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_delete_limited"), - ) - - -@bot.callback_query_handler(query_equals("add_data"), is_admin=True) -async def add_data_command(call: types.CallbackQuery): - msg = await bot.edit_message_text( - f"πŸ”‹ Enter Data Limit to increase or decrease (GB):", - call.message.chat.id, - call.message.message_id, - reply_markup=BotKeyboard.inline_cancel_action(), - ) - schedule_delete_message(call.message.chat.id, call.message.id) - schedule_delete_message(call.message.chat.id, msg.id) - # return bot.register_next_step_handler(call.message, add_data_step) - await bot.set_state(call.from_user.id, botStates.add_data, call.message.chat.id) - - -@bot.message_handler(state=botStates.add_data) -async def add_data_step(message): - try: - data_limit = float(message.text) - if not data_limit: - raise ValueError - except ValueError: - wait_msg = await bot.send_message( - message.chat.id, "❌ Data limit must be a number and not zero." - ) - schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, add_data_step) - schedule_delete_message(message.chat.id, message.message_id) - msg = await bot.send_message( - message.chat.id, - f"⚠️ Are you sure? this will change Data limit of all users according to " - f"{'+' if data_limit > 0 else '-'}{readable_size(abs(data_limit *1024*1024*1024))}", - parse_mode="html", - reply_markup=BotKeyboard.confirm_action("do_add_data", data_limit), - ) - await bot.delete_state(message.from_user.id, message.chat.id) - await cleanup_messages(message.chat.id) - schedule_delete_message(message.chat.id, msg.id) - - -@bot.callback_query_handler(query_equals("add_time"), is_admin=True) -async def add_time_command(call: types.CallbackQuery): - msg = await bot.edit_message_text( - f"πŸ“… Enter Days to increase or decrease expiry:", - call.message.chat.id, - call.message.message_id, - reply_markup=BotKeyboard.inline_cancel_action(), - ) - schedule_delete_message(call.message.chat.id, call.message.id) - schedule_delete_message(call.message.chat.id, msg.id) - # return bot.register_next_step_handler(call.message, add_time_step) - await bot.set_state(call.from_user.id, botStates.add_time, call.message.chat.id) - - -@bot.message_handler(state=botStates.add_time) -async def add_time_step(message): - try: - days = int(message.text) - if not days: - raise ValueError - except ValueError: - wait_msg = await bot.send_message( - message.chat.id, "❌ Days must be as a number and not zero." - ) - schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, add_time_step) - schedule_delete_message(message.chat.id, message.message_id) - msg = await bot.send_message( - message.chat.id, - f"⚠️ Are you sure? this will change Expiry Time of all users according to {days} Days", - parse_mode="html", - reply_markup=BotKeyboard.confirm_action("do_add_time", days), - ) - await cleanup_messages(message.chat.id) - schedule_delete_message(message.chat.id, msg.id) - - -@bot.callback_query_handler(query_startswith("inbound"), is_admin=True) -async def inbound_command(call: types.CallbackQuery): - await bot.edit_message_text( - f"Select inbound to *{call.data[8:].title()}* from all users", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.inbounds_menu(call.data, xray.config.inbounds_by_tag), - ) - - -@bot.callback_query_handler(query_startswith("confirm_inbound"), is_admin=True) -async def delete_expired_confirm_command(call: types.CallbackQuery): - await bot.edit_message_text( - f"⚠️ Are you sure? This will *{call.data[16:].replace(':', ' ')} for All Users*‼️", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action=call.data[8:]), - ) - - -@bot.callback_query_handler(query_startswith("edit:"), is_admin=True) -async def edit_command(call: types.CallbackQuery): - # bot.clear_step_handler_by_chat_id(call.message.chat.id) - await bot.delete_state(call.from_user.id, call.message.chat.id) - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, "❌ User not found.", show_alert=True - ) - services = crud.get_services(db) - user = UserResponse.model_validate(db_user) - mem_store.set(f"{call.message.chat.id}:username", username) - mem_store.set(f"{call.message.chat.id}:data_limit", db_user.data_limit) - mem_store.set( - f"{call.message.chat.id}:expire_date", - datetime.fromtimestamp(db_user.expire) if db_user.expire else None, - ) - mem_store.set(f"{call.message.chat.id}:services", [s.id for s in db_user.services]) - await bot.edit_message_text( - f"πŸ“ Editing user `{username}`", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.select_services( - services, - [s.id for s in user.services], - "edit", - username=username, - data_limit=db_user.data_limit, - expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"), - ), - ) - - -@bot.callback_query_handler(query_equals("help_edit"), is_admin=True) -async def help_edit_command(call: types.CallbackQuery): - await bot.answer_callback_query( - call.id, text="Press the (✏️ Edit) button to edit", show_alert=True - ) - - -@bot.callback_query_handler(query_equals("cancel"), is_admin=True) -async def cancel_command(call: types.CallbackQuery): - # bot.clear_step_handler_by_chat_id(call.message.chat.id) - await bot.delete_state(call.from_user.id, call.message.chat.id) - return await bot.edit_message_text( - get_system_info(), - call.message.chat.id, - call.message.message_id, - parse_mode="MarkdownV2", - reply_markup=BotKeyboard.main_menu(), - ) - - -@bot.callback_query_handler(query_startswith("edit_user:"), is_admin=True) -async def edit_user_command(call: types.CallbackQuery): - _, username, action = call.data.split(":") - schedule_delete_message(call.message.chat.id, call.message.id) - await cleanup_messages(call.message.chat.id) - if action == "data": - msg = await bot.send_message( - call.message.chat.id, - "⬆️ Enter Data Limit (GB):\n⚠️ Send 0 for unlimited.", - reply_markup=BotKeyboard.inline_cancel_action(f"user:{username}"), - ) - mem_store.set(f"{call.message.chat.id}:edit_msg_text", call.message.text) - await bot.delete_state(call.from_user.id, call.message.chat.id) - await bot.set_state( - call.from_user.id, botStates.edit_user_data_limit, call.message.chat.id - ) - async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - data["username"] = username - # bot.clear_step_handler_by_chat_id(call.message.chat.id) - # bot.register_next_step_handler( - # call.message, edit_user_data_limit_step, username) - schedule_delete_message(call.message.chat.id, msg.message_id) - elif action == "expire": - msg = await bot.send_message( - call.message.chat.id, - "⬆️ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\n⚠️ Send 0 for never expire.", - reply_markup=BotKeyboard.inline_cancel_action(f"user:{username}"), - ) - mem_store.set(f"{call.message.chat.id}:edit_msg_text", call.message.text) - # bot.clear_step_handler_by_chat_id(call.message.chat.id) - await bot.set_state( - call.from_user.id, botStates.edit_user_expire, call.message.chat.id - ) - async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - data["username"] = username - # bot.register_next_step_handler( - # call.message, edit_user_expire_step, username=username) - schedule_delete_message(call.message.chat.id, msg.message_id) - - -@bot.message_handler(state=botStates.edit_user_data_limit) -async def edit_user_data_limit_step(message: types.Message): - try: - if float(message.text) < 0: - wait_msg = await bot.send_message( - message.chat.id, "❌ Data limit must be greater or equal to 0." - ) - schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username) - data_limit = float(message.text) * 1024**3 - except ValueError: - wait_msg = await bot.send_message( - message.chat.id, "❌ Data limit must be a number." - ) - schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_data_limit_step, username=username) - mem_store.set(f"{message.chat.id}:data_limit", data_limit) - schedule_delete_message(message.chat.id, message.message_id) - text = mem_store.get(f"{message.chat.id}:edit_msg_text") - mem_store.delete(f"{message.chat.id}:edit_msg_text") - async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - username = data["username"] - del data["username"] - await bot.send_message( - message.chat.id, - text or f"πŸ“ Editing user {username}", - parse_mode="html", - reply_markup=BotKeyboard.select_protocols( - mem_store.get(f"{message.chat.id}:protocols"), - "edit", - username=username, - data_limit=data_limit, - expire_date=mem_store.get(f"{message.chat.id}:expire_date"), - ), - ) - await cleanup_messages(message.chat.id) - - -@bot.message_handler(state=botStates.edit_user_expire) -async def edit_user_expire_step(message: types.Message): - try: - now = datetime.now() - today = datetime( - year=now.year, month=now.month, day=now.day, hour=23, minute=59, second=59 - ) - if re.match(r"^[0-9]{1,3}(M|m|D|d)$", message.text): - expire_date = today - number_pattern = r"^[0-9]{1,3}" - number = int(re.findall(number_pattern, message.text)[0]) - symbol_pattern = r"(M|m|D|d)$" - symbol = re.findall(symbol_pattern, message.text)[0].upper() - if symbol == "M": - expire_date = today + relativedelta(months=number) - elif symbol == "D": - expire_date = today + relativedelta(days=number) - elif message.text != "0": - expire_date = datetime.strptime(message.text, "%Y-%m-%d") - else: - expire_date = None - if expire_date and expire_date < today: - wait_msg = await bot.send_message( - message.chat.id, "❌ Expire date must be greater than today." - ) - schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username) - except ValueError: - wait_msg = await bot.send_message( - message.chat.id, - "❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)", - ) - schedule_delete_message(message.chat.id, wait_msg.message_id) - return # bot.register_next_step_handler(wait_msg, edit_user_expire_step, username=username) - - mem_store.set(f"{message.chat.id}:expire_date", expire_date) - schedule_delete_message(message.chat.id, message.message_id) - text = mem_store.get(f"{message.chat.id}:edit_msg_text") - mem_store.delete(f"{message.chat.id}:edit_msg_text") - async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - username = data["username"] - del data["username"] - await bot.send_message( - message.chat.id, - text or f"πŸ“ Editing user {username}", - parse_mode="html", - reply_markup=BotKeyboard.select_protocols( - mem_store.get(f"{message.chat.id}:protocols"), - "edit", - username=username, - data_limit=mem_store.get(f"{message.chat.id}:data_limit"), - expire_date=expire_date, - ), - ) - await cleanup_messages(message.chat.id) - - -@bot.callback_query_handler(query_startswith("users:"), is_admin=True) -async def users_command(call: types.CallbackQuery): - page = int(call.data.split(":")[1]) if len(call.data.split(":")) > 1 else 1 - with GetDB() as db: - total_pages = math.ceil(crud.get_users_count(db) / 10) - users = crud.get_users( - db, - offset=(page - 1) * 10, - limit=10, - sort=[crud.UsersSortingOptions["-created_at"]], - ) - text = """πŸ‘₯ Users: (Page {page}/{total_pages}) -βœ… Active -❌ Disabled -πŸ•° Expired -πŸͺ« Limited""".format( - page=page, total_pages=total_pages - ) - - await bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_list(users, page, total_pages=total_pages), - ) - - -def get_user_info_text( - status: str, - username: str, - sub_url: str, - data_limit: int = None, - usage: int = None, - expire: int = None, - note: str = None, -) -> str: - statuses = {"active": "βœ…", "expired": "πŸ•°", "limited": "πŸͺ«", "disabled": "❌"} - text = f"""\ -β”Œβ”€{statuses[status]} Status: {status.title()} -β”‚ └─Username: {username} -β”‚ -β”œβ”€πŸ”‹ Data limit: {readable_size(data_limit) if data_limit else 'Unlimited'} -β”‚ └─Data Used: {readable_size(usage) if usage else "-"} -β”‚ -β”œβ”€πŸ“… Expiry Date: {datetime.fromtimestamp(expire).date() if expire else 'Never'} -β”‚ └─Days left: {(datetime.fromtimestamp(expire or 0) - datetime.now()).days if expire else '-'} -β”‚ -""" - if note: - text += f"β”œβ”€πŸ“ Note: {note}\nβ”‚\n" - text += f'β””β”€πŸš€ Subscription: {sub_url}' - return text - - -@bot.callback_query_handler(query_startswith("edit_note:"), is_admin=True) -async def edit_note_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, "❌ User not found.", show_alert=True - ) - schedule_delete_message(call.message.chat.id, call.message.id) - await cleanup_messages(call.message.chat.id) - msg = await bot.send_message( - call.message.chat.id, - f"πŸ“ Current Note: {db_user.note}\n\nSend new Note for {username}", - parse_mode="HTML", - reply_markup=BotKeyboard.inline_cancel_action(f"user:{username}"), - ) - mem_store.set(f"{call.message.chat.id}:username", username) - schedule_delete_message(call.message.chat.id, msg.id) - await bot.set_state(call.from_user.id, botStates.edit_note, call.message.chat.id) - # bot.register_next_step_handler(msg, edit_note_step) - - -@bot.message_handler(state=botStates.edit_note) -async def edit_note_step(message: types.Message): - note = message.text or "" - if len(note) > 500: - wait_msg = await bot.send_message( - message.chat.id, "❌ Note can not be more than 500 characters." - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, edit_note_step) - username = mem_store.get(f"{message.chat.id}:username") - if not username: - await cleanup_messages(message.chat.id) - await bot.reply_to(message, "❌ Something went wrong!\n restart bot /start") - with GetDB() as db: - db_user = crud.get_user(db, username) - last_note = db_user.note - db_user = crud.update_user( - db, db_user, UserModify(username=username, note=note) - ) - user = UserResponse.model_validate(db_user) - text = get_user_info_text( - status=user.status, - username=user.username, - sub_url=user.subscription_url, - expire=user.expire, - data_limit=user.data_limit, - usage=user.used_traffic, - note=user.note, - ) - await bot.reply_to( - message, - text, - parse_mode="html", - reply_markup=BotKeyboard.user_menu( - user_info={"status": user.status, "username": user.username}, note=True - ), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ“ #Edit_Note #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {username} -Last Note : {last_note} -New Note : {note} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {message.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler(query_startswith("user:"), is_admin=True) -async def user_command(call: types.CallbackQuery): - await bot.delete_state(call.from_user.id, call.message.chat.id) - # bot.clear_step_handler_by_chat_id(call.message.chat.id) - username = call.data.split(":")[1] - page = int(call.data.split(":")[2]) if len(call.data.split(":")) > 2 else 1 - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, "❌ User not found.", show_alert=True - ) - user = UserResponse.model_validate(db_user) - text = get_user_info_text( - status=user.status, - username=username, - sub_url=user.subscription_url, - data_limit=user.data_limit, - usage=user.used_traffic, - expire=user.expire, - note=user.note, - ) - await bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - {"username": user.username, "status": user.status}, page=page, note=True - ), - ) - - -@bot.callback_query_handler(query_startswith("revoke_sub:"), is_admin=True) -async def revoke_sub_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - await bot.edit_message_text( - f"⚠️ Are you sure? This will *Revoke Subscription* link for `{username}`‼️", - call.message.chat.id, - call.message.message_id, - parse_mode="markdown", - reply_markup=BotKeyboard.confirm_action(action="do_revoke", username=username), - ) - - -@bot.callback_query_handler(query_startswith("links:"), is_admin=True) -async def links_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, "User not found!", show_alert=True - ) - - user = UserResponse.model_validate(db_user) - - text = f"{user.subscription_url}\n\n\n" - for link in user.links: - text += f"{link}\n\n" - - await bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.show_links(username), - ) - - -@bot.callback_query_handler(query_startswith("genqr:"), is_admin=True) -async def genqr_command(call: types.CallbackQuery): - username = call.data.split(":")[1] - - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, "User not found!", show_alert=True - ) - - user = UserResponse.model_validate(db_user) - - await bot.answer_callback_query(call.id, "Generating QR code...") - - for link in user.links: - f = io.BytesIO() - qr = qrcode.QRCode(border=6) - qr.add_data(link) - qr.make_image().save(f) - f.seek(0) - await bot.send_photo( - call.message.chat.id, - photo=f, - caption=f"{link}", - parse_mode="HTML", - ) - with io.BytesIO() as f: - qr = qrcode.QRCode(border=6) - qr.add_data(user.subscription_url) - qr.make_image().save(f) - f.seek(0) - await bot.send_photo( - call.message.chat.id, - photo=f, - caption=get_user_info_text( - status=user.status, - username=user.username, - sub_url=user.subscription_url, - data_limit=user.data_limit, - usage=user.used_traffic, - expire=user.expire, - ), - parse_mode="HTML", - reply_markup=BotKeyboard.subscription_page(user.subscription_url), - ) - try: - await bot.delete_message(call.message.chat.id, call.message.message_id) - except: - pass - - text = f"{user.subscription_url}\n\n\n" - for link in user.links: - text += f"{link}\n\n" - - await bot.send_message( - call.message.chat.id, - text, - "HTML", - reply_markup=BotKeyboard.show_links(username), - ) - - -@bot.callback_query_handler(query_startswith("random"), is_admin=True) -async def random_username(call: types.CallbackQuery): - # bot.clear_step_handler_by_chat_id(call.message.chat.id) - await bot.delete_state(call.from_user.id, call.message.chat.id) - template_id = int(call.data.split(":")[1] or 0) - mem_store.delete(f"{call.message.chat.id}:template_id") - - characters = string.ascii_letters + string.digits - username = ( - random.choice(characters) - + "".join(random.choices(characters, k=4)) - + "_" - + "".join(random.choices(characters, k=4)) - ) - - schedule_delete_message(call.message.chat.id, call.message.id) - await cleanup_messages(call.message.chat.id) - - msg = await bot.send_message( - call.message.chat.id, - "⬆️ Enter Data Limit (GB):\n⚠️ Send 0 for unlimited.", - reply_markup=BotKeyboard.inline_cancel_action(), - ) - schedule_delete_message(call.message.chat.id, msg.id) - await bot.set_state( - call.from_user.id, botStates.add_user_data_limit, call.message.chat.id - ) - async with bot.retrieve_data(call.from_user.id, call.message.chat.id) as data: - data["username"] = username - return - - -@bot.callback_query_handler(query_equals("add_user"), is_admin=True) -async def add_user_command(call: types.CallbackQuery): - try: - await bot.delete_message(call.message.chat.id, call.message.message_id) - except: # noqa - pass - username_msg = await bot.send_message( - call.message.chat.id, - "πŸ‘€ Enter username:\n⚠️Username only can be 3 to 32 characters and contain a-z, A-Z 0-9, and underscores in " - "between.", - reply_markup=BotKeyboard.random_username(), - ) - schedule_delete_message(call.message.chat.id, username_msg.id) - await bot.set_state( - call.from_user.id, botStates.add_user_username, call.message.chat.id - ) - # bot.register_next_step_handler(username_msg, add_user_username_step) - - -@bot.message_handler(state=botStates.add_user_username) -async def add_user_username_step(message: types.Message): - username = message.text - if not username: - wait_msg = await bot.send_message( - message.chat.id, "❌ Username can not be empty." - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_username_step) - if not re.match(r"^(?!.*__)(?!.*_$)\w{2,31}[a-zA-Z\d]$", username): - wait_msg = await bot.send_message( - message.chat.id, - "❌ Username only can be 3 to 32 characters and contain a-z, A-Z, 0-9, and underscores in between.", - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_username_step) - with GetDB() as db: - if crud.get_user(db, username): - wait_msg = await bot.send_message( - message.chat.id, "❌ Username already exists." - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_username_step) - schedule_delete_message(message.chat.id, message.id) - await cleanup_messages(message.chat.id) - msg = await bot.send_message( - message.chat.id, - "⬆️ Enter Data Limit (GB):\n⚠️ Send 0 for unlimited.", - reply_markup=BotKeyboard.inline_cancel_action(), - ) - schedule_delete_message(message.chat.id, msg.id) - await bot.set_state( - message.from_user.id, botStates.add_user_data_limit, message.chat.id - ) - async with bot.retrieve_data(message.from_user.id, message.chat.id) as data: - data["username"] = username - # bot.register_next_step_handler(msg, add_user_data_limit_step, username=username) - - -@bot.message_handler(state=botStates.add_user_data_limit) -async def add_user_data_limit_step(message: types.Message): - try: - if float(message.text) < 0: - wait_msg = await bot.send_message( - message.chat.id, "❌ Data limit must be greater or equal to 0." - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username) - data_limit = float(message.text) * 1024 * 1024 * 1024 - except ValueError: - wait_msg = await bot.send_message( - message.chat.id, "❌ Data limit must be a number." - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_data_limit_step, username=username) - schedule_delete_message(message.chat.id, message.id) - await cleanup_messages(message.chat.id) - msg = await bot.send_message( - message.chat.id, - "⬆️ Enter Expire Date (YYYY-MM-DD)\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D) :\n⚠️ Send 0 for never expire.", - reply_markup=BotKeyboard.inline_cancel_action(), - ) - schedule_delete_message(message.chat.id, msg.id) - await bot.set_state( - message.from_user.id, botStates.add_user_expire, message.chat.id - ) - async with bot.retrieve_data(message.from_user.id, message.chat.id) as data: - data["data_limit"] = data_limit - - # bot.register_next_step_handler(msg, add_user_expire_step, username=username, data_limit=data_limit) - - -@bot.message_handler(state=botStates.add_user_expire) -async def add_user_expire_step(message: types.Message): - try: - now = datetime.now() - today = datetime( - year=now.year, month=now.month, day=now.day, hour=23, minute=59, second=59 - ) - if re.match(r"^[0-9]{1,3}(M|m|D|d)$", message.text): - expire_date = today - number_pattern = r"^[0-9]{1,3}" - number = int(re.findall(number_pattern, message.text)[0]) - symbol_pattern = r"(M|m|D|d)$" - symbol = re.findall(symbol_pattern, message.text)[0].upper() - if symbol == "M": - expire_date = today + relativedelta(months=number) - elif symbol == "D": - expire_date = today + relativedelta(days=number) - elif message.text != "0": - expire_date = datetime.strptime(message.text, "%Y-%m-%d") - else: - expire_date = None - if expire_date and expire_date < today: - wait_msg = await bot.send_message( - message.chat.id, "❌ Expire date must be greater than today." - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit) - except ValueError: - wait_msg = await bot.send_message( - message.chat.id, - "❌ Expire date must be in YYYY-MM-DD format.\nOr You Can Use Regex Symbol: ^[0-9]{1,3}(M|D)", - ) - schedule_delete_message(message.chat.id, wait_msg.id) - schedule_delete_message(message.chat.id, message.id) - return # bot.register_next_step_handler(wait_msg, add_user_expire_step, username=username, data_limit=data_limit) - async with bot.retrieve_data(message.from_user.id, message.chat.id) as data: - username, data_limit = data["username"], data["data_limit"] - mem_store.set(f"{message.chat.id}:username", username) - mem_store.set(f"{message.chat.id}:data_limit", data_limit) - mem_store.set(f"{message.chat.id}:expire_date", expire_date) - - with GetDB() as db: - services = crud.get_services(db) - schedule_delete_message(message.chat.id, message.id) - await cleanup_messages(message.chat.id) - await bot.send_message( - message.chat.id, - "Select Services:\nUsernames: {}\nData Limit: {}\nExpiry Date {}".format( - mem_store.get(f"{message.chat.id}:username"), - ( - readable_size(mem_store.get(f"{message.chat.id}:data_limit")) - if mem_store.get(f"{message.chat.id}:data_limit") - else "Unlimited" - ), - ( - mem_store.get(f"{message.chat.id}:expire_date").strftime("%Y-%m-%d") - if mem_store.get(f"{message.chat.id}:expire_date") - else "Never" - ), - ), - reply_markup=BotKeyboard.select_services(services, [], action="create"), - ) - - -@bot.callback_query_handler(query_startswith("switch_service:"), is_admin=True) -async def switch_service(call: types.CallbackQuery): - if not (username := mem_store.get(f"{call.message.chat.id}:username")): - return await bot.answer_callback_query( - call.id, "❌ No user selected.", show_alert=True - ) - selected_services: List[int] = mem_store.get(f"{call.message.chat.id}:services", []) - _, service, action = call.data.split(":") - service = int(service) - - with GetDB() as db: - services = crud.get_services(db) - if service in selected_services: - selected_services.remove(service) - else: - selected_services.append(service) - - mem_store.set(f"{call.message.chat.id}:services", selected_services) - - if action in ["edit"]: - return await bot.edit_message_text( - call.message.text, - call.message.chat.id, - call.message.message_id, - reply_markup=BotKeyboard.select_services( - services, - selected_services, - "edit", - username=username, - data_limit=mem_store.get(f"{call.message.chat.id}:data_limit"), - expire_date=mem_store.get(f"{call.message.chat.id}:expire_date"), - ), - ) - await bot.edit_message_text( - call.message.text, - call.message.chat.id, - call.message.message_id, - reply_markup=BotKeyboard.select_services(services, selected_services, "create"), - ) - - -@bot.callback_query_handler(query_startswith("confirm:"), is_admin=True) -async def confirm_user_command(call: types.CallbackQuery): - data = call.data.split(":")[1] - chat_id = call.from_user.id - full_name = call.from_user.full_name - now = datetime.now() - today = datetime( - year=now.year, month=now.month, day=now.day, hour=23, minute=59, second=59 - ) - - -@bot.callback_query_handler(query_startswith("do_delete"), is_admin=True) -async def delete_user(call: types.CallbackQuery): - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - crud.remove_user(db, db_user) - await xray.operations.remove_user(db_user) - - await bot.edit_message_text( - "βœ… User deleted.", - call.message.chat.id, - call.message.message_id, - reply_markup=BotKeyboard.main_menu(), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ—‘ #Deleted #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {db_user.username} -Traffic Limit : {readable_size(db_user.data_limit) if db_user.data_limit else "Unlimited"} -Expire Date : \ -{datetime.fromtimestamp(db_user.expire).strftime('%H:%M:%S %Y-%m-%d') if db_user.expire else "Never"} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler(query_startswith("do_suspend"), is_admin=True) -async def disable_user(call: types.CallbackQuery): - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - crud.update_user( - db, db_user, UserModify(username=username, status=UserStatusModify.disabled) - ) - await xray.operations.remove_user(db_user) - user = UserResponse.model_validate(db_user) - await bot.edit_message_text( - get_user_info_text( - status="disabled", - username=username, - sub_url=user.subscription_url, - data_limit=db_user.data_limit, - usage=db_user.used_traffic, - expire=db_user.expire, - note=user.note, - ), - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - user_info={"status": "disabled", "username": db_user.username}, note=True - ), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -❌ #Disabled #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {username} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler(query_startswith("do_activate"), is_admin=True) -async def activate_user(call: types.CallbackQuery): - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - crud.update_user( - db, db_user, UserModify(username=username, status=UserStatusModify.active) - ) - await xray.operations.add_user(db_user) - user = UserResponse.model_validate(db_user) - await bot.edit_message_text( - get_user_info_text( - status="active", - username=username, - sub_url=user.subscription_url, - data_limit=db_user.data_limit, - usage=db_user.used_traffic, - expire=db_user.expire, - note=user.note, - ), - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - user_info={"status": "active", "username": db_user.username}, note=True - ), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -βœ… #Activated #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {username} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler(query_startswith("do_reset_usage"), is_admin=True) -async def reset_all_usages(call: types.CallbackQuery): - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - crud.reset_user_data_usage(db, db_user) - user = UserResponse.model_validate(db_user) - await bot.edit_message_text( - get_user_info_text( - status=user.status, - username=username, - sub_url=user.subscription_url, - data_limit=user.data_limit, - usage=user.used_traffic, - expire=user.expire, - note=user.note, - ), - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - user_info={"status": user.status, "username": user.username}, note=True - ), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ” #Reset_usage #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {username} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler(query_equals("do_restart"), is_admin=True) -async def restart_xray(call: types.CallbackQuery): - m = await bot.edit_message_text( - "πŸ”„ Restarting XRay core...", call.message.chat.id, call.message.message_id - ) - await xray.core.restart(xray.config.include_db_users()) - for node_id, node in list(xray.nodes.items()): - if await node.is_healthy(): - await xray.operations.restart_node(node_id, xray.config.include_db_users()) - await bot.edit_message_text( - "βœ… XRay core restarted successfully.", - m.chat.id, - m.message_id, - reply_markup=BotKeyboard.main_menu(), - ) - - -@bot.callback_query_handler(query_equals("edit_user_done"), is_admin=True) -async def edit_user(call: types.CallbackQuery): - if (username := mem_store.get(f"{call.message.chat.id}:username")) is None: - try: - await bot.delete_message(call.message.chat.id, call.message.message_id) - except Exception: - pass - return await bot.send_message( - call.message.chat.id, - "❌ Bot reload detected. Please start over.", - reply_markup=BotKeyboard.main_menu(), - ) - - if not mem_store.get(f"{call.message.chat.id}:services"): - return await bot.answer_callback_query( - call.id, "❌ No Service selected.", show_alert=True - ) - - # inbounds: dict[str, list[str]] = { - # k: v for k, v in mem_store.get(f'{call.message.chat.id}:protocols').items() if v} - services: List[int] = mem_store.get(f"{call.message.chat.id}:services") - - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, text=f"User not found!", show_alert=True - ) - - # proxies = {p.type.value: p.settings for p in db_user.proxies} - - """for protocol in xray.config.inbounds_by_protocol: - if protocol in inbounds and protocol not in db_user.inbounds: - proxies.update({protocol: {'flow': TELEGRAM_DEFAULT_VLESS_FLOW} if - TELEGRAM_DEFAULT_VLESS_FLOW and protocol == ProxyTypes.VLESS else {}}) - elif protocol in db_user.inbounds and protocol not in inbounds: - del proxies[protocol]""" - - modify = UserModify( - username=username, - # status=db_user.status, - expire=( - int(mem_store.get(f"{call.message.chat.id}:expire_date").timestamp()) - if mem_store.get(f"{call.message.chat.id}:expire_date") - else 0 - ), - data_limit=mem_store.get(f"{call.message.chat.id}:data_limit"), - services=services, - ) - last_user = UserResponse.model_validate(db_user) - db_user = crud.update_user(db, db_user, modify) - - user = UserResponse.model_validate(db_user) - - if user.status == UserStatus.active: - await xray.operations.update_user(db_user) - else: - await xray.operations.remove_user(db_user) - - await bot.answer_callback_query(call.id, "βœ… User updated successfully.") - - text = get_user_info_text( - status=user.status, - username=user.username, - sub_url=user.subscription_url, - data_limit=user.data_limit, - usage=user.used_traffic, - expire=user.expire, - note=user.note, - ) - await bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - {"username": db_user.username, "status": db_user.status}, note=True - ), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - tag = f'\nβž–βž–βž–βž–βž–βž–βž–βž–βž– \nBy : {call.from_user.full_name}' - if last_user.data_limit != user.data_limit: - text = f"""\ -πŸ“Ά #Traffic_Change #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {user.username} -Last Traffic Limit : {readable_size(last_user.data_limit) if last_user.data_limit else "Unlimited"} -New Traffic Limit : {readable_size(user.data_limit) if user.data_limit else "Unlimited"}{tag}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - if last_user.expire != user.expire: - text = f"""\ -πŸ“… #Expiry_Change #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {user.username} -Last Expire Date : \ -{datetime.fromtimestamp(last_user.expire).strftime('%H:%M:%S %Y-%m-%d') if last_user.expire else "Never"} -New Expire Date : \ -{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"}{tag}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - if list(last_user.inbounds.values())[0] != list(user.inbounds.values())[0]: - text = f"""\ -βš™οΈ #Inbounds_Change #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {user.username} -Last Proxies : {", ".join(list(last_user.inbounds.values())[0])} -New Proxies : {", ".join(list(user.inbounds.values())[0])}{tag}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler(query_startswith("add_user_done"), is_admin=True) -async def delete_limpired(call: types.CallbackQuery): - if mem_store.get(f"{call.message.chat.id}:username") is None: - try: - await bot.delete_message(call.message.chat.id, call.message.message_id) - except Exception: - pass - return await bot.send_message( - call.message.chat.id, - "❌ Bot reload detected. Please start over.", - reply_markup=BotKeyboard.main_menu(), - ) - - if not mem_store.get(f"{call.message.chat.id}:services"): - return await bot.answer_callback_query( - call.id, "❌ No services selected.", show_alert=True - ) - - services: List[int] = mem_store.get(f"{call.message.chat.id}:services", []) - - new_user = UserCreate( - username=mem_store.get(f"{call.message.chat.id}:username"), - expire=( - int(mem_store.get(f"{call.message.chat.id}:expire_date").timestamp()) - if mem_store.get(f"{call.message.chat.id}:expire_date") - else None - ), - data_limit=( - mem_store.get(f"{call.message.chat.id}:data_limit") - if mem_store.get(f"{call.message.chat.id}:data_limit") - else None - ), - services=services, - ) - try: - with GetDB() as db: - db_user = crud.create_user(db, new_user) - services = db_user.services - user = UserResponse.model_validate(db_user) - except sqlalchemy.exc.IntegrityError: - db.rollback() - return await bot.answer_callback_query( - call.id, "❌ Username already exists.", show_alert=True - ) - - await xray.operations.add_user(db_user) - - text = get_user_info_text( - status=user.status, - username=user.username, - sub_url=user.subscription_url, - data_limit=user.data_limit, - usage=user.used_traffic, - expire=user.expire, - note=user.note, - ) - await bot.edit_message_text( - text, - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - user_info={"status": user.status, "username": user.username}, note=True - ), - ) - - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ†• #Created #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username : {user.username} -Traffic Limit : {readable_size(user.data_limit) if user.data_limit else "Unlimited"} -Expire Date : \ -{datetime.fromtimestamp(user.expire).strftime('%H:%M:%S %Y-%m-%d') if user.expire else "Never"} -Services : {"" if not services else ", ".join([s.name for s in services])} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.callback_query_handler( - query_startswith(["do_delete_expired", "do_delete_limited"]), is_admin=True -) -async def delete_limpired(call: types.CallbackQuery): - await bot.edit_message_text( - "⏳ In Progress...", - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - ) - with GetDB() as db: - depleted_users = crud.get_users( - db, - status=[ - UserStatus.limited if data == "delete_limited" else UserStatus.expired - ], - ) - file_name = f"{data[8:]}_users_{int(now.timestamp()*1000)}.txt" - with open(file_name, "w") as f: - f.write("USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n") - deleted = 0 - for user in depleted_users: - try: - crud.remove_user(db, user) - await xray.operations.remove_user(user) - deleted += 1 - f.write( - f'{user.username}\ -\t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\ -\t{readable_size(user.used_traffic) if user.used_traffic else 0}\ -/{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\ -\t{user.status}\n' - ) - except: - db.rollback() - await bot.edit_message_text( - f"βœ… {deleted}/{len(depleted_users)} {data[7:].title()} Users Deleted", - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.main_menu(), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ—‘ #Delete #{data[7:].title()} #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Count: {deleted} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_document( - TELEGRAM_LOGGER_CHANNEL_ID, - open(file_name, "rb"), - caption=text, - parse_mode="HTML", - ) - os.remove(file_name) - except: - pass - - -@bot.callback_query_handler(query_startswith("do_add_data:"), is_admin=True) -async def add_data_to_all(call: types.CallbackQuery): - schedule_delete_message( - call.message.chat.id, - (await bot.send_message(chat_id, "⏳ In Progress...", "HTML")).id, - ) - data_limit = float(call.data.split(":")[1]) * 1024 * 1024 * 1024 - with GetDB() as db: - users = crud.get_users(db) - counter = 0 - file_name = f"new_data_limit_users_{int(now.timestamp()*1000)}.txt" - with open(file_name, "w") as f: - f.write("USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n") - for user in users: - try: - if user.data_limit and user.status not in [ - UserStatus.limited, - UserStatus.expired, - ]: - user = crud.update_user( - db, - user, - UserModify( - username=user.username, - data_limit=(user.data_limit + data_limit), - ), - ) - counter += 1 - f.write( - f'{user.username}\ -\t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\ -\t{readable_size(user.used_traffic) if user.used_traffic else 0}\ -/{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\ -\t{user.status}\n' - ) - except: - db.rollback() - await cleanup_messages(chat_id) - await bot.send_message( - chat_id, - f'βœ… {counter}/{len(users)} Users Data Limit according to {"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))}', - "HTML", - reply_markup=BotKeyboard.main_menu(), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ“Ά #Traffic_Change #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -According to: {"+" if data_limit > 0 else "-"}{readable_size(abs(data_limit))} -Count: {counter} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_document( - TELEGRAM_LOGGER_CHANNEL_ID, - open(file_name, "rb"), - caption=text, - parse_mode="HTML", - ) - os.remove(file_name) - except: - pass - - -@bot.callback_query_handler(query_startswith("do_add_time:"), is_admin=True) -async def add_time_to_all(call: types.CallbackQuery): - schedule_delete_message( - call.message.chat.id, - (await bot.send_message(chat_id, "⏳ In Progress...", "HTML")).id, - ) - days = int(call.data.split(":")[1]) - with GetDB() as db: - users = crud.get_users(db) - counter = 0 - file_name = f"new_expiry_users_{int(now.timestamp()*1000)}.txt" - with open(file_name, "w") as f: - f.write("USERNAME\tEXIPRY\tUSAGE/LIMIT\tSTATUS\n") - for user in users: - try: - if user.expire and user.status not in [ - UserStatus.limited, - UserStatus.expired, - ]: - user = crud.update_user( - db, - user, - UserModify( - username=user.username, - expire=int( - ( - datetime.fromtimestamp(user.expire) - + relativedelta(days=days) - ).timestamp() - ), - ), - ) - counter += 1 - f.write( - f'{user.username}\ -\t{datetime.fromtimestamp(user.expire) if user.expire else "never"}\ -\t{readable_size(user.used_traffic) if user.used_traffic else 0}\ -/{readable_size(user.data_limit) if user.data_limit else "Unlimited"}\ -\t{user.status}\n' - ) - except: - db.rollback() - await cleanup_messages(chat_id) - await bot.send_message( - chat_id, - f"βœ… {counter}/{len(users)} Users Expiry Changes according to {days} Days", - "HTML", - reply_markup=BotKeyboard.main_menu(), - ) - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -πŸ“… #Expiry_Change #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -According to: {days} Days -Count: {counter} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_document( - TELEGRAM_LOGGER_CHANNEL_ID, - open(file_name, "rb"), - caption=text, - parse_mode="HTML", - ) - os.remove(file_name) - except: - pass - - -@bot.callback_query_handler(query_startswith("do_revoke:"), is_admin=True) -async def revoke_sub(call: types.CallbackQuery): - username = call.data.split(":")[1] - with GetDB() as db: - db_user = crud.get_user(db, username) - if not db_user: - return await bot.answer_callback_query( - call.id, text=f"User not found!", show_alert=True - ) - db_user = crud.revoke_user_sub(db, db_user) - user = UserResponse.model_validate(db_user) - text = get_user_info_text( - status=user.status, - username=user.username, - sub_url=user.subscription_url, - expire=user.expire, - data_limit=user.data_limit, - usage=user.used_traffic, - note=user.note, - ) - await bot.edit_message_text( - f"βœ… Subscription Successfully Revoked!\n\n{text}", - call.message.chat.id, - call.message.message_id, - parse_mode="HTML", - reply_markup=BotKeyboard.user_menu( - user_info={"status": user.status, "username": user.username}, note=True - ), - ) - - if TELEGRAM_LOGGER_CHANNEL_ID: - text = f"""\ -🚫 #Revoke_sub #From_Bot -βž–βž–βž–βž–βž–βž–βž–βž–βž– -Username: {username} -βž–βž–βž–βž–βž–βž–βž–βž–βž– -By : {call.from_user.full_name}""" - try: - await bot.send_message(TELEGRAM_LOGGER_CHANNEL_ID, text, "HTML") - except: - pass - - -@bot.message_handler(func=lambda message: True, is_admin=True) -async def search(message: types.Message): - with GetDB() as db: - db_user = crud.get_user(db, message.text) - if not db_user: - return await bot.reply_to(message, "❌ User not found.") - user = UserResponse.model_validate(db_user) - text = get_user_info_text( - status=user.status, - username=user.username, - sub_url=user.subscription_url, - expire=user.expire, - data_limit=user.data_limit, - usage=user.used_traffic, - note=user.note, - ) - return await bot.reply_to( - message, - text, - parse_mode="html", - reply_markup=BotKeyboard.user_menu( - user_info={"status": user.status, "username": user.username}, note=True - ), - ) diff --git a/app/telegram/handlers/user.py b/app/telegram/handlers/user.py deleted file mode 100644 index 696b2e9..0000000 --- a/app/telegram/handlers/user.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import datetime -from app.db import GetDB, crud -from app.models.user import UserResponse -from app.telegram import bot - -# from pytz import UTC -from telebot.custom_filters import ChatFilter -from telebot.util import extract_arguments - -from app.utils.system import readable_size - -bot.add_custom_filter(ChatFilter()) - - -@bot.message_handler(commands=["usage"]) -def usage_command(message): - username = extract_arguments(message.text) - if not username: - return bot.reply_to( - message, "Usage: `/usage `", parse_mode="MarkdownV2" - ) - - with GetDB() as db: - dbuser = crud.get_user(db, username) - - if not dbuser: - return bot.reply_to(message, "No user found with this username") - user = UserResponse.model_validate(dbuser) - - statuses = {"active": "βœ…", "expired": "πŸ•°", "limited": "πŸ“΅", "disabled": "❌"} - - text = f"""\ -β”Œβ”€{statuses[user.status]} Status: {user.status.title()} -β”‚ └─Username: {user.username} -β”‚ -β”œβ”€πŸ”‹ Data limit: {readable_size(user.data_limit) if user.data_limit else 'Unlimited'} -β”‚ └─Data Used: {readable_size(user.used_traffic) if user.used_traffic else "-"} -β”‚ -β””β”€πŸ“… Expiry Date: {datetime.fromtimestamp(user.expire).date() if user.expire else 'Never'} - └─Days left: {(datetime.fromtimestamp(user.expire or 0) - datetime.now()).days if user.expire else '-'}""" - - return bot.reply_to(message, text, parse_mode="HTML") diff --git a/app/telegram/handlers/report.py b/app/telegram/report.py similarity index 85% rename from app/telegram/handlers/report.py rename to app/telegram/report.py index bb1ab51..8a73630 100644 --- a/app/telegram/handlers/report.py +++ b/app/telegram/report.py @@ -4,8 +4,7 @@ from datetime import datetime from telebot.apihelper import ApiTelegramException from telebot.formatting import escape_html -from app.telegram import bot -from app.telegram.utils.keyboard import BotKeyboard +from app import telegram from app.utils.system import readable_size from config import TELEGRAM_ADMIN_ID, TELEGRAM_LOGGER_CHANNEL_ID @@ -13,15 +12,15 @@ logger = logging.getLogger(__name__) async def report(message: str, parse_mode="html", keyboard=None): - if bot and (TELEGRAM_ADMIN_ID or TELEGRAM_LOGGER_CHANNEL_ID): + if telegram.bot and (TELEGRAM_ADMIN_ID or TELEGRAM_LOGGER_CHANNEL_ID): try: if TELEGRAM_LOGGER_CHANNEL_ID: - await bot.send_message( + await telegram.bot.send_message( TELEGRAM_LOGGER_CHANNEL_ID, message, parse_mode=parse_mode ) else: for admin in TELEGRAM_ADMIN_ID: - await bot.send_message( + await telegram.bot.send_message( admin, message, parse_mode=parse_mode, reply_markup=keyboard ) except ApiTelegramException as e: @@ -54,12 +53,7 @@ async def report_new_user( services="" if not services else ", ".join([s.name for s in services]), ) - return await report( - text, - keyboard=BotKeyboard.user_menu( - {"username": username, "id": user_id, "status": "active"}, with_back=False - ), - ) + return await report(text) async def report_user_modification( @@ -84,12 +78,7 @@ async def report_user_modification( services=", ".join([s.name for s in services]), ) - return await report( - text, - keyboard=BotKeyboard.user_menu( - {"username": username, "status": "active"}, with_back=False - ), - ) + return await report(text) async def report_user_deletion(username: str, by: str): @@ -108,9 +97,8 @@ async def report_user_deletion(username: str, by: str): async def report_status_change(username: str, status: str): _status = { "active": "βœ… #Activated", - "disabled": "❌ #Disabled", - "limited": "πŸͺ« #Limited", "on_hold": "🟑 #On_Hold", + "limited": "πŸͺ« #Limited", "expired": "πŸ•” #Expired", } text = """\ diff --git a/app/telegram/utils/__init__.py b/app/telegram/utils/__init__.py deleted file mode 100644 index 6699e5e..0000000 --- a/app/telegram/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from app.telegram.utils import custom_filters - - -def setup() -> None: - custom_filters.setup() diff --git a/app/telegram/utils/custom_filters.py b/app/telegram/utils/custom_filters.py deleted file mode 100644 index 9d0fc82..0000000 --- a/app/telegram/utils/custom_filters.py +++ /dev/null @@ -1,35 +0,0 @@ -from app.telegram import bot -from typing import Union - -from telebot import types -from telebot.asyncio_filters import AdvancedCustomFilter, StateFilter - -from config import TELEGRAM_ADMIN_ID - - -class IsAdminFilter(AdvancedCustomFilter): - key = "is_admin" - - async def check(self, message, text): - """ - :meta private: - """ - if isinstance(message, types.CallbackQuery): - return message.from_user.id in TELEGRAM_ADMIN_ID - return message.chat.id in TELEGRAM_ADMIN_ID - - -def query_equals(text: str): - return lambda query: query.data == text - - -def query_startswith(text: Union[list, str]): - if type(text) is str: - return lambda query: query.data.startswith(text) - else: - return lambda query: query.data.startswith(tuple(text)) - - -def setup() -> None: - bot.add_custom_filter(IsAdminFilter()) - bot.add_custom_filter(StateFilter(bot)) diff --git a/app/telegram/utils/keyboard.py b/app/telegram/utils/keyboard.py deleted file mode 100644 index 421f990..0000000 --- a/app/telegram/utils/keyboard.py +++ /dev/null @@ -1,286 +0,0 @@ -from datetime import datetime as dt -from itertools import islice -from typing import Literal, Dict, List - -from telebot import types # noqa - -from app.utils.system import readable_size - - -def chunk_dict(data: dict, size: int = 2): - it = iter(data) - for i in range(0, len(data), size): - yield {k: data[k] for k in islice(it, size)} - - -class BotKeyboard: - - @staticmethod - def main_menu(): - keyboard = types.InlineKeyboardMarkup() - keyboard.add( - types.InlineKeyboardButton(text="πŸ” System Info", callback_data="system"), - types.InlineKeyboardButton(text="♻️ Restart Xray", callback_data="restart"), - ) - keyboard.add( - types.InlineKeyboardButton(text="πŸ‘₯ Users", callback_data="users:1"), - types.InlineKeyboardButton( - text="✏️ Edit All Users", callback_data="edit_all" - ), - ) - # keyboard.add( - # types.InlineKeyboardButton(text='βž• Create User From Template', callback_data='template_add_user')) - keyboard.add( - types.InlineKeyboardButton(text="βž• Create User", callback_data="add_user") - ) - return keyboard - - @staticmethod - def edit_all_menu(): - keyboard = types.InlineKeyboardMarkup() - keyboard.add( - types.InlineKeyboardButton( - text="πŸ—‘ Delete Expired", callback_data="delete_expired" - ), - types.InlineKeyboardButton( - text="πŸ—‘ Delete Limited", callback_data="delete_limited" - ), - ) - keyboard.add( - types.InlineKeyboardButton( - text="πŸ”‹ Data (βž•|βž–)", callback_data="add_data" - ), - types.InlineKeyboardButton( - text="πŸ“… Time (βž•|βž–)", callback_data="add_time" - ), - ) - # keyboard.add( - # types.InlineKeyboardButton(text='βž• Add Inbound', callback_data='inbound_add'), - # types.InlineKeyboardButton(text='βž– Remove Inbound', callback_data='inbound_remove')) - keyboard.add(types.InlineKeyboardButton(text="πŸ”™ Back", callback_data="cancel")) - return keyboard - - # @staticmethod - # def inbounds_menu(action, inbounds): - # keyboard = types.InlineKeyboardMarkup() - # for inbound in inbounds: - # keyboard.add(types.InlineKeyboardButton(text=inbound, callback_data=f'confirm_{action}:{inbound}')) - # keyboard.add(types.InlineKeyboardButton(text='πŸ”™ Back', callback_data='cancel')) - # return keyboard - - @staticmethod - def random_username(template_id: str = ""): - keyboard = types.InlineKeyboardMarkup() - - keyboard.add( - types.InlineKeyboardButton( - text="πŸ”‘ Random Username", callback_data=f"random:{template_id}" - ) - ) - keyboard.add( - types.InlineKeyboardButton(text="πŸ”™ Cancel", callback_data="cancel") - ) - return keyboard - - @staticmethod - def user_menu(user_info, with_back: bool = True, page: int = 1, note: bool = False): - keyboard = types.InlineKeyboardMarkup() - keyboard.add( - types.InlineKeyboardButton( - text="❌ Disable" if user_info["status"] == "active" else "βœ… Activate", - callback_data=f"{'suspend' if user_info['status'] == 'active' else 'activate'}:{user_info['username']}", - ), - types.InlineKeyboardButton( - text="πŸ—‘ Delete", callback_data=f"delete:{user_info['username']}" - ), - ) - if note: - keyboard.add( - types.InlineKeyboardButton( - text="🚫 Revoke Sub", - callback_data=f"revoke_sub:{user_info['username']}", - ), - types.InlineKeyboardButton( - text="✏️ Edit", callback_data=f"edit:{user_info['username']}" - ), - ) - keyboard.add( - types.InlineKeyboardButton( - text="πŸ“ Edit Note", - callback_data=f"edit_note:{user_info['username']}", - ), - types.InlineKeyboardButton( - text="πŸ“‘ Links", callback_data=f"links:{user_info['username']}" - ), - ) - else: - keyboard.add( - types.InlineKeyboardButton( - text="πŸ“‘ Links", callback_data=f"links:{user_info['username']}" - ), - types.InlineKeyboardButton( - text="✏️ Edit", callback_data=f"edit:{user_info['username']}" - ), - ) - keyboard.add( - types.InlineKeyboardButton( - text="πŸ” Reset usage", - callback_data=f"reset_usage:{user_info['username']}", - ), - types.InlineKeyboardButton( - text="πŸ”‹ Charge", callback_data=f"charge:{user_info['username']}" - ), - ) - if with_back: - keyboard.add( - types.InlineKeyboardButton( - text="πŸ”™ Back", callback_data=f"users:{page}" - ) - ) - return keyboard - - @staticmethod - def show_links(username: str): - keyboard = types.InlineKeyboardMarkup() - - keyboard.add( - types.InlineKeyboardButton( - text="πŸ–Ό QR code", callback_data=f"genqr:{username}" - ) - ) - keyboard.add( - types.InlineKeyboardButton(text="πŸ”™ Back", callback_data=f"user:{username}") - ) - return keyboard - - @staticmethod - def subscription_page(sub_url: str): - keyboard = types.InlineKeyboardMarkup() - if sub_url[:4] == "http": - keyboard.add( - types.InlineKeyboardButton(text="πŸš€ Subscription Page", url=sub_url) - ) - return keyboard - - @staticmethod - def confirm_action(action: str, username: str = None): - keyboard = types.InlineKeyboardMarkup() - keyboard.add( - types.InlineKeyboardButton( - text="Yes", callback_data=f"{action}:{username}" - ), - types.InlineKeyboardButton(text="No", callback_data=f"cancel"), - ) - return keyboard - - @staticmethod - def inline_cancel_action(callback_data: str = "cancel"): - keyboard = types.InlineKeyboardMarkup() - keyboard.add( - types.InlineKeyboardButton(text="πŸ”™ Cancel", callback_data=callback_data) - ) - return keyboard - - @staticmethod - def user_list(users: list, page: int, total_pages: int): - keyboard = types.InlineKeyboardMarkup() - if len(users) >= 2: - users = [p for p in users] - users = [users[i : i + 2] for i in range(0, len(users), 2)] - else: - users = [users] - for user in users: - row = [] - for p in user: - status = { - "active": "βœ…", - "expired": "πŸ•°", - "limited": "πŸ“΅", - "disabled": "❌", - } - row.append( - types.InlineKeyboardButton( - text=f"{p.username} ({status[p.status]})", - callback_data=f"user:{p.username}:{page}", - ) - ) - keyboard.row(*row) - # if there is more than one page - if total_pages > 1: - if page > 1: - keyboard.add( - types.InlineKeyboardButton( - text="⬅️ Previous", callback_data=f"users:{page - 1}" - ) - ) - if page < total_pages: - keyboard.add( - types.InlineKeyboardButton( - text="➑️ Next", callback_data=f"users:{page + 1}" - ) - ) - keyboard.add(types.InlineKeyboardButton(text="πŸ”™ Back", callback_data="cancel")) - return keyboard - - @staticmethod - def select_services( - services, - selected_services: List[int], - action: Literal["edit", "create"], - username: str = None, - data_limit: float = None, - expire_date: dt = None, - ): - - keyboard = types.InlineKeyboardMarkup() - - if action == "edit": - keyboard.add( - types.InlineKeyboardButton( - text="⚠️ Data Limit:", callback_data=f"help_edit" - ) - ) - keyboard.add( - types.InlineKeyboardButton( - text=f"{readable_size(data_limit) if data_limit else 'Unlimited'}", - callback_data=f"help_edit", - ), - types.InlineKeyboardButton( - text="✏️ Edit", callback_data=f"edit_user:{username}:data" - ), - ) - keyboard.add( - types.InlineKeyboardButton( - text="πŸ“… Expire Date:", callback_data=f"help_edit" - ) - ) - keyboard.add( - types.InlineKeyboardButton( - text=f"{expire_date.strftime('%Y-%m-%d') if expire_date else 'Never'}", - callback_data=f"help_edit", - ), - types.InlineKeyboardButton( - text="✏️ Edit", callback_data=f"edit_user:{username}:expire" - ), - ) - for s in services: - keyboard.add( - types.InlineKeyboardButton( - text=f"🌐 {s.name} {'βœ…' if s.id in selected_services else '❌'}", - callback_data=f"switch_service:{s.id}:{action}", - ) - ) - keyboard.add( - types.InlineKeyboardButton( - text="Done", - callback_data="edit_user_done" if action == "edit" else "add_user_done", - ) - ) - keyboard.add( - types.InlineKeyboardButton( - text="Cancel", - callback_data=f"user:{username}" if action == "edit" else "cancel", - ) - ) - - return keyboard