├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── keyboxGenerator.py ├── logo.jpg ├── main.py ├── requirements.txt └── user_data.json /.env.example: -------------------------------------------------------------------------------- 1 | TELEGRAM_BOT_TOKEN=YOUR_ACTUAL_BOT_TOKEN_HERE 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.pyc 4 | *.pyo 5 | *.pyd 6 | 7 | # Distribution / packaging 8 | build/ 9 | dist/ 10 | *.egg-info/ 11 | .eggs/ 12 | 13 | # Environments 14 | .env 15 | venv/ 16 | 17 | # Other 18 | keybox.xml 19 | ecPrivateKey.pem 20 | certificate.pem 21 | rsaPrivateKey.pem 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 [CRZX1337] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Logo 3 |
4 |

5 | 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | 8 | A Telegram bot that generates Android `keybox.xml` files for device attestation. Built with `python-telegram-bot` and OpenSSL. Features user limits, a VIP system, and an admin panel for managing users. 9 | 10 | ## ✨ Features 11 | 12 | * **⚡️ Instant Keybox Generation:** Create `keybox.xml` files with `/generate` or a button tap. 13 | * **🤖 Interactive Interface:** Uses Telegram's inline keyboards. 14 | * **📦 File & Text Output:** Receive the keybox as a file or as text in the chat. 15 | * **🧐 Clear Error Handling:** Informative error messages. 16 | * **❓ Help & Source Code:** `/help` command links to the GitHub repo. 17 | * **🔒 Secure Configuration:** Uses a `.env` file for the bot token. 18 | * **⚙️ Easy Setup:** `requirements.txt` for easy dependency installation. 19 | * **👤 User Limits:** Regular users have a daily limit (default: 5 keyboxes). 20 | * **👑 VIP Status:** Admin can grant VIP status to remove limits. 21 | * **🛡️ Admin Panel:** `/admin` command (for admin user) to manage users and view data. 22 | * **💾 Data Persistence:** User data is saved to `user_data.json`. 23 | 24 | ## 📝 Requirements 25 | 26 | * Python 3.7+ 27 | * `python-telegram-bot[all]` (Install: `pip install -r requirements.txt`) 28 | * `python-dotenv` (Install: `pip install -r requirements.txt`) 29 | * OpenSSL (usually pre-installed; see below) 30 | 31 | ## ⬇️ Installation and Usage 32 | 33 | 1. **Clone:** 34 | ```bash 35 | git clone https://github.com/CRZX1337/Keybox-Generator-Telegram-Bot.git 36 | cd Keybox-Generator-Telegram-Bot 37 | ``` 38 | 39 | 2. **Virtual Environment (Recommended):** 40 | ```bash 41 | python3 -m venv venv 42 | source venv/bin/activate # Linux/macOS 43 | venv\Scripts\activate # Windows 44 | ``` 45 | 46 | 3. **Install Dependencies:** 47 | ```bash 48 | pip install -r requirements.txt 49 | ``` 50 | 51 | 4. **`.env` File (Secret!):** 52 | ``` 53 | TELEGRAM_BOT_TOKEN=YOUR_BOT_TOKEN_HERE 54 | ``` 55 | **Important:** `.env` file MUST be in your `.gitignore`. 56 | 57 | 5. **Set Admin User ID:** 58 | Open `main.py` and change the value of `ADMIN_USER_ID` to *your* Telegram user ID. 59 | 60 | 6. **Run:** 61 | ```bash 62 | python main.py 63 | ``` 64 | 65 | 7. **Telegram:** 66 | * `/start`: Welcome message. 67 | * `/generate`: Create a keybox (or use the button). 68 | * `/help`: Get help. 69 | * `/admin`: Access the admin panel (if you're the admin). 70 | 71 | ## Obtaining a Telegram Bot Token 72 | 73 | 1. Open Telegram, search for **@BotFather**. 74 | 2. Send `/newbot`. 75 | 3. Follow prompts, and paste the given Token in the `.env` file. 76 | 77 | ## OpenSSL Installation (if needed) 78 | 79 | * **Debian/Ubuntu:** `sudo apt-get update && sudo apt-get install openssl` 80 | * **Fedora/CentOS/RHEL:** `sudo yum install openssl` 81 | * **macOS:** `brew install openssl` (with Homebrew) 82 | * **Windows:** Download OpenSSL (e.g., [https://slproweb.com/products/Win32OpenSSL.html](https://slproweb.com/products/Win32OpenSSL.html)). Ensure `openssl` is in your PATH. 83 | 84 | ## 🤝 Contributing 85 | 86 | Contributions welcome! 87 | 88 | ## 📜 License 89 | 90 | MIT License (see the [LICENSE](LICENSE) file). 91 | -------------------------------------------------------------------------------- /keyboxGenerator.py: -------------------------------------------------------------------------------- 1 | import os 2 | from random import randint, choice 3 | from base64 import b64decode 4 | 5 | try: 6 | os.chdir(os.path.abspath(os.path.dirname(__file__))) 7 | except: 8 | pass 9 | 10 | EXIT_SUCCESS = 0 11 | EXIT_FAILURE = 1 12 | EOF = (-1) 13 | LB = 2 14 | UB = 12 15 | CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 16 | keyboxFormatter = """ 17 | 18 | 1 19 | 20 | 21 | 22 | {1} 23 | 24 | 1 25 | 26 | {2} 27 | 28 | 29 | 30 | 31 | {3} 32 | 33 | 34 | 35 | """ 36 | 37 | 38 | def canOverwrite(flags: list, idx: int, prompts: str | tuple | list | set) -> bool: 39 | if ( 40 | isinstance(flags, list) 41 | and isinstance(idx, int) 42 | and -len(flags) <= idx < len(flags) 43 | and isinstance(prompts, (str, tuple, list, set)) 44 | ): 45 | try: 46 | if isinstance(prompts, str): 47 | print('"{0}"'.format(prompts)) 48 | choice = input("The file mentioned above exists. Overwrite or not [aYn]? ") 49 | else: 50 | print(prompts) 51 | choice = input( 52 | "At least one of the files mentioned above exists. Overwrite or not [aYn]? " 53 | ) 54 | if choice.upper() == "A": 55 | for i in range( 56 | (idx if idx >= 0 else len(flags) + idx), len(flags) 57 | ): # overwirte the current file and all the following necessary files no matter whether they exist 58 | flags[i] = True 59 | return True 60 | elif choice.upper() == "N": 61 | return False 62 | else: 63 | flags[idx] = True 64 | return True 65 | except BaseException as e: 66 | print(e) 67 | return False 68 | else: 69 | input("#") 70 | return False 71 | 72 | 73 | def execute(commandline: str) -> int | None: 74 | if isinstance(commandline, str): 75 | print("$ " + commandline) 76 | return os.system(commandline) 77 | else: 78 | return None 79 | 80 | 81 | def handleOpenSSL(flag: bool = True) -> bool | None: 82 | if isinstance(flag, bool): 83 | errorLevel = execute("openssl version") 84 | if EXIT_SUCCESS == errorLevel: 85 | return True 86 | elif flag: # can try again 87 | execute("sudo apt-get install openssl libssl-dev") 88 | return handleOpenSSL(False) 89 | else: 90 | return False 91 | else: 92 | return None 93 | 94 | 95 | def pressTheEnterKeyToExit(errorLevel: int | None = None): 96 | try: 97 | print( 98 | "Please press the enter key to exit ({0}). ".format(errorLevel) 99 | if isinstance(errorLevel, int) 100 | else "Please press the enter key to exit. " 101 | ) 102 | input() 103 | except: 104 | pass 105 | 106 | def generate_keybox(ecPrivateKeyFilePath, certificateFilePath, rsaPrivateKeyFilePath): 107 | failureCount = 0 108 | deviceID = "".join([choice(CHARSET) for _ in range(randint(LB, UB))]) 109 | # First-phase Generation # 110 | failureCount += ( 111 | execute( 112 | 'openssl ecparam -name prime256v1 -genkey -noout -out "{0}"'.format( 113 | ecPrivateKeyFilePath 114 | ) 115 | ) 116 | != 0 117 | ) 118 | failureCount += ( 119 | execute( 120 | 'openssl req -new -x509 -key "{0}" -out {1} -days 3650 -subj "/CN=Keybox"'.format( 121 | ecPrivateKeyFilePath, certificateFilePath 122 | ) 123 | ) 124 | != 0 125 | ) 126 | failureCount += ( 127 | execute('openssl genrsa -out "{0}" 2048'.format(rsaPrivateKeyFilePath)) != 0 128 | ) 129 | if failureCount > 0: 130 | return "Error: Cannot generate a sample ``keybox.xml`` file since {0} PEM file{1} not generated successfully. ".format( 131 | failureCount, ("s were" if failureCount > 1 else " was") 132 | ) 133 | 134 | # First-phase Reading # 135 | try: 136 | with open(ecPrivateKeyFilePath, "r", encoding="utf-8") as f: 137 | ecPrivateKey = f.read() 138 | with open(certificateFilePath, "r", encoding="utf-8") as f: 139 | certificate = f.read() 140 | with open(rsaPrivateKeyFilePath, "r", encoding="utf-8") as f: 141 | rsaPrivateKey = f.read() 142 | except BaseException as e: 143 | return "Error: Failed to read one or more of the PEM files. Details are as follows. \n{0}".format( 144 | e 145 | ) 146 | 147 | # Second-phase Generation # 148 | if rsaPrivateKey.startswith("-----BEGIN PRIVATE KEY-----") and rsaPrivateKey.rstrip().endswith( 149 | "-----END PRIVATE KEY-----" 150 | ): 151 | print( 152 | "A newer openssl version is used. The RSA private key in the PKCS8 format will be converted to that in the PKCS1 format soon. " 153 | ) 154 | failureCount += execute( 155 | 'openssl rsa -in "{0}" -out "{0}" -traditional'.format(rsaPrivateKeyFilePath) 156 | ) 157 | if failureCount > 0: 158 | return "Error: Cannot convert the RSA private key in the PKCS8 format to that in the PKCS1 format. " 159 | else: 160 | print( 161 | "Finished converting the RSA private key in the PKCS8 format to that in the PKCS1 format. " 162 | ) 163 | try: 164 | with open(rsaPrivateKeyFilePath, "r", encoding="utf-8") as f: 165 | rsaPrivateKey = f.read() 166 | except BaseException as e: 167 | return 'Error: Failed to update the RSA private key from "{0}". Details are as follows. \n{1}'.format( 168 | rsaPrivateKeyFilePath, e 169 | ) 170 | elif rsaPrivateKey.startswith( 171 | "-----BEGIN OPENSSH PRIVATE KEY-----" 172 | ) and rsaPrivateKey.rstrip().endswith("-----END OPENSSH PRIVATE KEY-----"): 173 | print( 174 | "An OpenSSL private key is detected, which will be converted to the RSA private key soon. " 175 | ) 176 | failureCount += execute( 177 | 'ssh-keygen -p -m PEM -f "{0}" -N ""'.format(rsaPrivateKeyFilePath) 178 | ) 179 | if failureCount > 0: 180 | return "Error: Cannot convert the OpenSSL private key to the RSA private key. " 181 | else: 182 | print("Finished converting the OpenSSL private key to the RSA private key. ") 183 | try: 184 | with open( 185 | rsaPrivateKeyFilePath, "r", encoding="utf-8" 186 | ) as f: # the ``ssh-keygen`` overwrites the file though no obvious output filepaths specified 187 | rsaPrivateKey = f.read() 188 | except BaseException as e: 189 | return 'Error: Failed to update the RSA private key from "{0}". Details are as follows. \n{1}'.format( 190 | rsaPrivateKeyFilePath, e 191 | ) 192 | 193 | # Brief Checks # 194 | if not ( 195 | ecPrivateKey.startswith("-----BEGIN EC PRIVATE KEY-----") 196 | and ecPrivateKey.rstrip().endswith("-----END EC PRIVATE KEY-----") 197 | ): 198 | return "Error: An invalid EC private key is detected. Please try to use the latest key generation tools to solve this issue. " 199 | if not ( 200 | certificate.startswith("-----BEGIN CERTIFICATE-----") 201 | and certificate.rstrip().endswith("-----END CERTIFICATE-----") 202 | ): 203 | return "Error: An invalid certificate is detected. Please try to use the latest key generation tools to solve this issue. " 204 | if not ( 205 | rsaPrivateKey.startswith("-----BEGIN RSA PRIVATE KEY-----") 206 | and rsaPrivateKey.rstrip().endswith("-----END RSA PRIVATE KEY-----") 207 | ): 208 | return "Error: An invalid final RSA private key is detected. Please try to use the latest key generation tools to solve this issue. " 209 | 210 | # Keybox Generation # 211 | keybox = keyboxFormatter.format(deviceID, ecPrivateKey, certificate, rsaPrivateKey) 212 | return keybox 213 | 214 | def main( 215 | ecPrivateKeyFilePath: str = "ecPrivateKey.pem", 216 | certificateFilePath: str = "certificate.pem", 217 | rsaPrivateKeyFilePath: str = "rsaPrivateKey.pem", 218 | keyboxFilePath: str = "keybox.xml", 219 | ) -> str: 220 | # Generate Keybox (using helper function) 221 | keybox_content = generate_keybox(ecPrivateKeyFilePath, certificateFilePath, rsaPrivateKeyFilePath) 222 | if keybox_content.startswith("Error"): 223 | return keybox_content # Return error message 224 | 225 | # Write to file if no errors and path is provided. 226 | if keyboxFilePath: 227 | try: 228 | with open(keyboxFilePath, "w", encoding="utf-8") as f: 229 | f.write(keybox_content) 230 | return f"Successfully wrote the keybox to {keyboxFilePath}." 231 | 232 | except Exception as e: 233 | return f"Failed to write the keybox to {keyboxFilePath}. Details:\n{e}" 234 | else: 235 | return keybox_content # if keyboxFilePath not provided return the generated key 236 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CRZX1337/Keybox-Generator-Telegram-Bot/ae0e12047fcc1c2825b8529f931c2719c34d9f71/logo.jpg -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import json 4 | import time 5 | from datetime import datetime, timedelta, timezone 6 | 7 | from telegram import Update, ForceReply, InlineKeyboardMarkup, InlineKeyboardButton 8 | from telegram.ext import ( 9 | Application, 10 | CommandHandler, 11 | MessageHandler, 12 | CallbackContext, 13 | filters, 14 | CallbackQueryHandler, 15 | ) 16 | from dotenv import load_dotenv 17 | import keyboxGenerator 18 | 19 | # Enable logging 20 | logging.basicConfig( 21 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO 22 | ) 23 | logger = logging.getLogger(__name__) 24 | 25 | # --- Constants and Configuration --- 26 | ADMIN_USER_ID = 5685799208 # Replace with your actual user ID 27 | DAILY_LIMIT = 5 28 | LIMIT_DURATION_HOURS = 24 29 | DATA_FILE = "user_data.json" 30 | 31 | # Load environment variables 32 | load_dotenv() 33 | TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") 34 | 35 | # --- Data Management Functions --- 36 | 37 | def load_data(): 38 | """Loads user data from the JSON file.""" 39 | try: 40 | with open(DATA_FILE, "r") as f: 41 | return json.load(f) 42 | except (FileNotFoundError, json.JSONDecodeError): 43 | return {} 44 | 45 | def save_data(data): 46 | """Saves user data to the JSON file.""" 47 | with open(DATA_FILE, "w") as f: 48 | json.dump(data, f, indent=4) 49 | 50 | def get_user_data(user_id, data): 51 | """Gets user data, creating a default entry if needed.""" 52 | user_id_str = str(user_id) # Ensure string for JSON keys 53 | if user_id_str not in data: 54 | data[user_id_str] = { 55 | "count": 0, 56 | "last_reset": int(time.time()), # Unix timestamp 57 | "vip": False 58 | } 59 | return data[user_id_str] 60 | 61 | 62 | def check_and_reset_limit(user_data): 63 | """Checks if the time limit has expired and resets the count if needed.""" 64 | 65 | current_time = int(time.time()) 66 | last_reset_time = user_data["last_reset"] 67 | 68 | if current_time - last_reset_time >= LIMIT_DURATION_HOURS * 3600: 69 | user_data["count"] = 0 70 | user_data["last_reset"] = current_time 71 | return True # indicates limit has reset 72 | return False 73 | 74 | 75 | def escape_markdown_v2(text: str) -> str: 76 | """Escapes special characters for MarkdownV2.""" 77 | escape_chars = r"_*[]()~`>#+-=|{}.!" 78 | return "".join(("\\" + char if char in escape_chars else char) for char in text) 79 | 80 | # --- Telegram Bot Handlers --- 81 | 82 | async def start(update: Update, context: CallbackContext) -> None: 83 | """Send a message when the command /start is issued.""" 84 | user = update.effective_user 85 | keyboard = [ 86 | [InlineKeyboardButton("Generate Keybox", callback_data="generate")], 87 | ] 88 | reply_markup = InlineKeyboardMarkup(keyboard) 89 | 90 | message = ( 91 | f"Hi {user.first_name}! 👋\n\n" 92 | "Welcome to the Keybox Generator Bot! I can create `keybox.xml` files " 93 | "used for Android device attestation.\n\n" 94 | "Click the button below to get started, or use /help for more information." 95 | ) 96 | 97 | await update.message.reply_text( 98 | escape_markdown_v2(message), reply_markup=reply_markup, parse_mode="MarkdownV2" 99 | ) 100 | 101 | 102 | async def generate_keybox_command(update: Update, context: CallbackContext) -> None: 103 | """Generates the keybox and sends it, checking limits.""" 104 | query = update.callback_query 105 | user_id = update.effective_user.id 106 | 107 | # Load data, get user, check/reset limit, check vip 108 | data = load_data() 109 | user_data = get_user_data(user_id, data) 110 | limit_reset = check_and_reset_limit(user_data) 111 | 112 | 113 | if user_data["vip"]: 114 | limit_message = "👑 You are a VIP user. Unlimited keybox generation!" 115 | 116 | elif user_data["count"] >= DAILY_LIMIT: 117 | # Calculate remaining time 118 | current_time = int(time.time()) 119 | last_reset_time = user_data["last_reset"] 120 | time_remaining_seconds = LIMIT_DURATION_HOURS * 3600 - (current_time - last_reset_time) 121 | time_remaining = timedelta(seconds = time_remaining_seconds) 122 | 123 | limit_message = ( 124 | f"❌ You have reached your daily limit of {DAILY_LIMIT} keyboxes.\n" 125 | f"Time remaining until reset: {time_remaining}" 126 | ) 127 | if query: 128 | await query.answer() 129 | await query.edit_message_text(escape_markdown_v2(limit_message)) 130 | else: 131 | await update.message.reply_text(escape_markdown_v2(limit_message)) 132 | return # Stop here if the limit is reached 133 | 134 | else: 135 | 136 | limit_message = f"🔑 Keyboxes generated today: {user_data['count']}/{DAILY_LIMIT}" 137 | if(limit_reset): 138 | limit_message = f"✅ Your daily Keybox limit has been reset\n{limit_message}" 139 | # Proceed with keybox generation 140 | if query: 141 | await query.answer() # Always answer! 142 | await query.edit_message_text(text=f"Generating keybox.xml...\n{limit_message}") 143 | else: 144 | await update.message.reply_text(f"Generating keybox.xml...\n{limit_message}") 145 | 146 | result = keyboxGenerator.main() 147 | 148 | if result.startswith("Successfully"): 149 | user_data["count"] += 1 # Increment count 150 | save_data(data) # Save the updated count *before* sending 151 | with open("keybox.xml", "rb") as f: 152 | if query: 153 | await query.message.reply_document(document=f, filename="keybox.xml") 154 | else: 155 | await update.message.reply_document(document=f, filename="keybox.xml") 156 | 157 | # Success message with options 158 | keyboard = [ 159 | [InlineKeyboardButton("Generate Another Keybox", callback_data="generate")], 160 | [InlineKeyboardButton("Show Help", callback_data="help")], 161 | ] 162 | reply_markup = InlineKeyboardMarkup(keyboard) 163 | success_message = f"✅ Keybox generated successfully!\n{limit_message}\nWhat would you like to do next?" 164 | 165 | if query: 166 | await query.message.reply_text( 167 | escape_markdown_v2(success_message), reply_markup=reply_markup 168 | ) 169 | else: 170 | await update.message.reply_text( 171 | escape_markdown_v2(success_message), reply_markup=reply_markup 172 | ) 173 | elif "keybox" in result.lower() and " None: 208 | """Shows the help message.""" 209 | keyboard = [ 210 | [ 211 | InlineKeyboardButton( 212 | "View Source Code (GitHub)", 213 | url="https://github.com/CRZX1337/Keybox-Generator-Telegram-Bot", 214 | ) 215 | ], 216 | ] 217 | reply_markup = InlineKeyboardMarkup(keyboard) 218 | 219 | message = ( 220 | "This bot generates Android keybox.xml files.\n\n" 221 | "**Commands:**\n\n" 222 | "/start - Start the bot and see the welcome message.\n" 223 | "/generate - Create a new keybox.xml file.\n" 224 | "/help - Show this help message.\n\n" 225 | "Click the button below to view the source code on GitHub." 226 | ) 227 | 228 | query = update.callback_query 229 | if query: 230 | await query.answer() 231 | await query.edit_message_text( 232 | escape_markdown_v2(message), reply_markup=reply_markup, parse_mode="MarkdownV2" 233 | ) 234 | else: 235 | await update.message.reply_text( 236 | escape_markdown_v2(message), reply_markup=reply_markup, parse_mode="MarkdownV2" 237 | ) 238 | 239 | 240 | 241 | 242 | async def button(update: Update, context: CallbackContext) -> None: 243 | """Parses the CallbackQuery and updates the message text.""" 244 | query = update.callback_query 245 | await query.answer() 246 | 247 | if query.data == "generate": 248 | await generate_keybox_command(update, context) 249 | elif query.data == "help": 250 | await help_command(update, context) 251 | else: 252 | await query.edit_message_text(text=f"Selected option: {query.data}") 253 | 254 | 255 | 256 | # --- Admin Commands --- 257 | 258 | async def admin_panel(update: Update, context: CallbackContext) -> None: 259 | """Displays the admin panel.""" 260 | user_id = update.effective_user.id 261 | if user_id != ADMIN_USER_ID: 262 | await update.message.reply_text("Unauthorized.") 263 | return 264 | 265 | keyboard = [ 266 | [InlineKeyboardButton("List Users", callback_data="admin_list")], 267 | [InlineKeyboardButton("Add VIP", callback_data="admin_add_vip")], 268 | [InlineKeyboardButton("Remove VIP", callback_data="admin_remove_vip")], 269 | [InlineKeyboardButton("Show Limit", callback_data="admin_show_limit")] 270 | ] 271 | reply_markup = InlineKeyboardMarkup(keyboard) 272 | await update.message.reply_text("Admin Panel:", reply_markup=reply_markup) 273 | 274 | 275 | async def admin_list_users(update: Update, context: CallbackContext) -> None: 276 | """Lists all users and their data (admin only).""" 277 | query = update.callback_query 278 | await query.answer() 279 | 280 | if query.from_user.id != ADMIN_USER_ID: 281 | await query.edit_message_text("Unauthorized.") 282 | return 283 | data = load_data() 284 | 285 | if not data: 286 | await query.edit_message_text("No user data found.") 287 | return 288 | user_list = "" 289 | 290 | for user_id, user_data in data.items(): 291 | user_list += ( 292 | f"ID: {user_id}, Count: {user_data['count']}, " 293 | f"Last Reset: {datetime.fromtimestamp(user_data['last_reset'])}, VIP: {user_data['vip']}\n" 294 | ) 295 | await query.edit_message_text(f"User List:\n{user_list}") 296 | 297 | async def admin_add_vip(update: Update, context: CallbackContext) -> None: 298 | """Adds a VIP user (admin only).""" 299 | query = update.callback_query 300 | await query.answer() 301 | 302 | if query.from_user.id != ADMIN_USER_ID: 303 | await query.edit_message_text("Unauthorized.") 304 | return 305 | 306 | await query.edit_message_text("Enter the User ID to add as VIP:") 307 | context.user_data["admin_action"] = "add_vip" 308 | 309 | 310 | async def admin_remove_vip(update: Update, context: CallbackContext) -> None: 311 | """Removes a VIP user (admin only).""" 312 | query = update.callback_query 313 | await query.answer() 314 | 315 | if query.from_user.id != ADMIN_USER_ID: 316 | await query.edit_message_text("Unauthorized.") 317 | return 318 | await query.edit_message_text("Enter the User ID to remove from VIP:") 319 | context.user_data["admin_action"] = "remove_vip" 320 | 321 | async def admin_show_limit(update: Update, context: CallbackContext) -> None: 322 | """Removes a VIP user (admin only).""" 323 | query = update.callback_query 324 | await query.answer() 325 | 326 | if query.from_user.id != ADMIN_USER_ID: 327 | await query.edit_message_text("Unauthorized.") 328 | return 329 | message = f"The current limits are:\n-Limit: {DAILY_LIMIT}\n-Duration: {LIMIT_DURATION_HOURS} hours" 330 | await query.edit_message_text(message) 331 | 332 | async def handle_admin_input(update: Update, context: CallbackContext) -> None: 333 | """Handles input for admin commands (e.g., adding/removing VIPs).""" 334 | user_id = update.effective_user.id 335 | if user_id != ADMIN_USER_ID: 336 | await update.message.reply_text("Unauthorized.") 337 | return 338 | 339 | if "admin_action" not in context.user_data: 340 | # Not in an admin action flow, just ignore. 341 | return 342 | 343 | 344 | text = update.message.text 345 | admin_action = context.user_data.pop("admin_action") # Remove after use 346 | 347 | if admin_action == "add_vip": 348 | try: 349 | vip_user_id = int(text) 350 | data = load_data() 351 | user_data = get_user_data(vip_user_id,data) # creates if doesn exist 352 | user_data["vip"] = True 353 | save_data(data) 354 | await update.message.reply_text(f"User {vip_user_id} added to VIPs.") 355 | 356 | except ValueError: 357 | await update.message.reply_text("Invalid User ID format.") 358 | 359 | elif admin_action == "remove_vip": 360 | try: 361 | vip_user_id = int(text) 362 | data = load_data() 363 | if str(vip_user_id) not in data: 364 | await update.message.reply_text("User Id not found") 365 | return 366 | data[str(vip_user_id)]["vip"] = False 367 | save_data(data) 368 | await update.message.reply_text(f"User {vip_user_id} removed from VIPs.") 369 | 370 | except ValueError: 371 | await update.message.reply_text("Invalid User ID format.") 372 | 373 | 374 | 375 | async def admin_button(update: Update, context: CallbackContext) -> None: 376 | """Parses the CallbackQuery and updates the message text.""" 377 | query = update.callback_query 378 | await query.answer() 379 | if query.from_user.id != ADMIN_USER_ID: 380 | await query.edit_message_text("Unauthorized.") 381 | return 382 | 383 | if query.data == "admin_list": 384 | await admin_list_users(update, context) 385 | elif query.data == "admin_add_vip": 386 | await admin_add_vip(update, context) 387 | elif query.data == "admin_remove_vip": 388 | await admin_remove_vip(update, context) 389 | elif query.data == "admin_show_limit": 390 | await admin_show_limit(update, context) 391 | else: 392 | await query.edit_message_text(text=f"Selected option: {query.data}") 393 | 394 | def main() -> None: 395 | """Start the bot.""" 396 | application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() 397 | 398 | application.add_handler(CommandHandler("start", start)) 399 | application.add_handler(CommandHandler("help", help_command)) 400 | application.add_handler(CommandHandler("generate", generate_keybox_command)) 401 | application.add_handler(CommandHandler("admin", admin_panel)) # admin panel 402 | application.add_handler(CallbackQueryHandler(button, pattern='^(?!admin_)')) # Handles buttons except "admin_" 403 | application.add_handler(CallbackQueryHandler(admin_button, pattern='^admin_')) # Handle admin panel buttons 404 | application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_admin_input)) # get input 405 | 406 | application.run_polling() 407 | 408 | 409 | if __name__ == "__main__": 410 | main() -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-telegram-bot[all] 2 | python-dotenv 3 | -------------------------------------------------------------------------------- /user_data.json: -------------------------------------------------------------------------------- 1 | {} --------------------------------------------------------------------------------