├── .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 |

3 |
4 |
5 |
6 | [](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 "" in result.lower():
174 | user_data["count"] += 1 # Increment count
175 | save_data(data)
176 | if query:
177 | await query.message.reply_text(f"Generated keybox (sent as text):\n{limit_message}")
178 | await query.message.reply_text(
179 | f"Generated Keybox:\n```{escape_markdown_v2(result)}```", parse_mode="MarkdownV2"
180 | )
181 | else:
182 |
183 | await update.message.reply_text(f"Generated keybox (sent as text):\n{limit_message}")
184 | await update.message.reply_text(
185 | f"Generated Keybox:\n```{escape_markdown_v2(result)}```", parse_mode="MarkdownV2"
186 | )
187 |
188 | # Success message with options
189 | keyboard = [
190 | [InlineKeyboardButton("Generate Another Keybox", callback_data="generate")],
191 | [InlineKeyboardButton("Show Help", callback_data="help")],
192 | ]
193 | reply_markup = InlineKeyboardMarkup(keyboard)
194 | success_message = "✅ Keybox generated successfully! What would you like to do next?"
195 |
196 | if query:
197 |
198 | await query.message.reply_text(escape_markdown_v2(success_message), reply_markup=reply_markup)
199 | else:
200 | await update.message.reply_text(escape_markdown_v2(success_message), reply_markup=reply_markup)
201 | else:
202 | if query:
203 | await query.message.reply_text(escape_markdown_v2(result)) # Escape error
204 | else:
205 | await update.message.reply_text(escape_markdown_v2(result))
206 |
207 | async def help_command(update: Update, context: CallbackContext) -> 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 | {}
--------------------------------------------------------------------------------