Skip to content

Commit 08e3ca6

Browse files
committed
Fix /addserver command stability and improve URL escaping
- Fix conversation handler timeout issues that caused /addserver to stop working - Add 5-minute timeout to prevent stuck conversations - Enhance input validation for server name, API URL, and SHA256 - Add duplicate server prevention (same API URL) - Improve error handling and database session management - Add comprehensive logging for debugging - Add new /status command for admin monitoring - Refactor all Telegram messages to use MarkdownV2 with proper escaping - Escape all access URLs and dynamic content to prevent markdown injection - Update scheduler notifications to use proper escaping - Add comprehensive test suite for URL escaping functionality - Improve conversation state management and cleanup - Add graceful error recovery with user-friendly messages
1 parent 75f7e5d commit 08e3ca6

File tree

2 files changed

+175
-38
lines changed

2 files changed

+175
-38
lines changed

bot/handlers/admin.py

Lines changed: 154 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,23 @@ async def start_add_server(update: Update, context: ContextTypes.DEFAULT_TYPE) -
170170

171171
async def get_server_name(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
172172
"""Get server name from user."""
173+
if not update.message or not update.message.text:
174+
await update.message.reply_text("❌ Please send a text message with the server name.")
175+
return WAITING_SERVER_NAME
176+
173177
server_name = update.message.text.strip()
174178

175179
if not server_name:
176180
await update.message.reply_text("❌ Server name cannot be empty. Please enter a valid name:")
177181
return WAITING_SERVER_NAME
178182

183+
if len(server_name) > 100:
184+
await update.message.reply_text("❌ Server name too long. Please enter a name with less than 100 characters:")
185+
return WAITING_SERVER_NAME
186+
179187
context.user_data['server_name'] = server_name
188+
logger.info(f"User {update.effective_user.id} set server name: {server_name}")
189+
180190
await update.message.reply_text(
181191
f"✅ Server name set to: {server_name}\n\n"
182192
"Now please enter the API URL (e.g., https://your-server.com:12345/access-keys):"
@@ -186,6 +196,10 @@ async def get_server_name(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
186196

187197
async def get_api_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
188198
"""Get API URL from user."""
199+
if not update.message or not update.message.text:
200+
await update.message.reply_text("❌ Please send a text message with the API URL.")
201+
return WAITING_API_URL
202+
189203
api_url = update.message.text.strip()
190204

191205
if not api_url.startswith(('http://', 'https://')):
@@ -194,7 +208,13 @@ async def get_api_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int
194208
)
195209
return WAITING_API_URL
196210

211+
if len(api_url) > 500:
212+
await update.message.reply_text("❌ URL too long. Please enter a shorter URL:")
213+
return WAITING_API_URL
214+
197215
context.user_data['api_url'] = api_url
216+
logger.info(f"User {update.effective_user.id} set API URL: {api_url}")
217+
198218
await update.message.reply_text(
199219
f"✅ API URL set to: {api_url}\n\n"
200220
"Finally, please enter the certificate SHA256 fingerprint:"
@@ -204,6 +224,10 @@ async def get_api_url(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int
204224

205225
async def get_cert_sha256(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
206226
"""Get certificate SHA256 and save the server."""
227+
if not update.message or not update.message.text:
228+
await update.message.reply_text("❌ Please send a text message with the certificate SHA256.")
229+
return WAITING_CERT_SHA256
230+
207231
cert_sha256 = update.message.text.strip()
208232

209233
if len(cert_sha256) != 64:
@@ -212,36 +236,73 @@ async def get_cert_sha256(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
212236
)
213237
return WAITING_CERT_SHA256
214238

215-
# Save the server
216-
async with get_db_session() as session:
217-
server = Server(
218-
name=context.user_data['server_name'],
219-
api_url=context.user_data['api_url'],
220-
cert_sha256=cert_sha256
239+
# Validate that it's a valid hex string
240+
try:
241+
int(cert_sha256, 16)
242+
except ValueError:
243+
await update.message.reply_text(
244+
"❌ Invalid SHA256 format. Please enter a valid hexadecimal SHA256 hash:"
221245
)
222-
session.add(server)
223-
await session.commit()
224-
225-
await log_action(
226-
update.effective_user.id,
227-
"server_added",
228-
"server",
229-
str(server.id),
230-
f"Added server {server.name}"
246+
return WAITING_CERT_SHA256
247+
248+
# Check that we have all required data
249+
if 'server_name' not in context.user_data or 'api_url' not in context.user_data:
250+
await update.message.reply_text(
251+
"❌ Missing server information. Please start over with /addserver"
231252
)
232-
233-
escaped_name = escape_markdown(server.name, version=2)
234-
escaped_api_url = escape_markdown(server.api_url, version=2)
235-
escaped_cert = escape_markdown(cert_sha256[:16] + "...", version=2)
236-
253+
context.user_data.clear()
254+
return ConversationHandler.END
255+
256+
# Save the server
257+
try:
258+
async with get_db_session() as session:
259+
# Check if a server with the same API URL already exists
260+
existing_server = await session.execute(
261+
select(Server).where(Server.api_url == context.user_data['api_url'])
262+
)
263+
if existing_server.scalar_one_or_none():
264+
await update.message.reply_text(
265+
"❌ A server with this API URL already exists. Please use a different URL."
266+
)
267+
return WAITING_API_URL
268+
269+
server = Server(
270+
name=context.user_data['server_name'],
271+
api_url=context.user_data['api_url'],
272+
cert_sha256=cert_sha256
273+
)
274+
session.add(server)
275+
await session.commit()
276+
277+
logger.info(f"User {update.effective_user.id} added server: {server.name} (ID: {server.id})")
278+
279+
await log_action(
280+
update.effective_user.id,
281+
"server_added",
282+
"server",
283+
str(server.id),
284+
f"Added server {server.name}"
285+
)
286+
287+
escaped_name = escape_markdown(server.name, version=2)
288+
escaped_api_url = escape_markdown(server.api_url, version=2)
289+
escaped_cert = escape_markdown(cert_sha256[:16] + "...", version=2)
290+
291+
await update.message.reply_text(
292+
f"✅ Server added successfully\\!\n\n"
293+
f"**Name:** {escaped_name}\n"
294+
f"**ID:** {server.id}\n"
295+
f"**API URL:** {escaped_api_url}\n"
296+
f"**Cert SHA256:** {escaped_cert}",
297+
parse_mode='MarkdownV2'
298+
)
299+
except Exception as e:
300+
logger.error(f"Error adding server for user {update.effective_user.id}: {e}")
237301
await update.message.reply_text(
238-
f"✅ Server added successfully\\!\n\n"
239-
f"**Name:** {escaped_name}\n"
240-
f"**ID:** {server.id}\n"
241-
f"**API URL:** {escaped_api_url}\n"
242-
f"**Cert SHA256:** {escaped_cert}",
243-
parse_mode='MarkdownV2'
302+
"❌ An error occurred while adding the server. Please try again later."
244303
)
304+
context.user_data.clear()
305+
return ConversationHandler.END
245306

246307
# Clear conversation data
247308
context.user_data.clear()
@@ -255,6 +316,72 @@ async def cancel_add_server(update: Update, context: ContextTypes.DEFAULT_TYPE)
255316
return ConversationHandler.END
256317

257318

319+
async def timeout_add_server(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
320+
"""Handle conversation timeout for add server."""
321+
if update.message:
322+
await update.message.reply_text(
323+
"⏰ Server addition timed out due to inactivity. Please run /addserver again to start over."
324+
)
325+
context.user_data.clear()
326+
return ConversationHandler.END
327+
328+
329+
@admin_only
330+
async def bot_status(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
331+
"""Show bot status information for debugging."""
332+
try:
333+
async with get_db_session() as session:
334+
# Count users by role
335+
users_result = await session.execute(select(User))
336+
users = users_result.scalars().all()
337+
338+
user_counts = {
339+
"pending": sum(1 for u in users if u.role == UserRole.PENDING),
340+
"sales": sum(1 for u in users if u.role == UserRole.SALES),
341+
"blocked": sum(1 for u in users if u.role == UserRole.BLOCKED),
342+
"admin": sum(1 for u in users if u.role == UserRole.ADMIN),
343+
}
344+
345+
# Count servers
346+
servers_result = await session.execute(select(Server))
347+
servers = servers_result.scalars().all()
348+
active_servers = sum(1 for s in servers if s.is_active)
349+
350+
# Count access keys
351+
keys_result = await session.execute(select(AccessKey))
352+
keys = keys_result.scalars().all()
353+
354+
now = datetime.utcnow()
355+
active_keys = sum(1 for k in keys if k.disabled_at is None and k.expires_at > now)
356+
expired_keys = sum(1 for k in keys if k.disabled_at is None and k.expires_at <= now)
357+
disabled_keys = sum(1 for k in keys if k.disabled_at is not None)
358+
359+
status_message = (
360+
f"🤖 **Bot Status**\n\n"
361+
f"👥 **Users:**\n"
362+
f" • Total: {len(users)}\n"
363+
f" • Pending: {user_counts['pending']}\n"
364+
f" • Sales: {user_counts['sales']}\n"
365+
f" • Blocked: {user_counts['blocked']}\n"
366+
f" • Admin: {user_counts['admin']}\n\n"
367+
f"🖥️ **Servers:**\n"
368+
f" • Total: {len(servers)}\n"
369+
f" • Active: {active_servers}\n\n"
370+
f"🔑 **Access Keys:**\n"
371+
f" • Total: {len(keys)}\n"
372+
f" • Active: {active_keys}\n"
373+
f" • Expired: {expired_keys}\n"
374+
f" • Disabled: {disabled_keys}\n\n"
375+
f"⏰ **Timestamp:** {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC"
376+
)
377+
378+
await update.message.reply_text(status_message)
379+
380+
except Exception as e:
381+
logger.error(f"Error getting bot status: {e}")
382+
await update.message.reply_text(f"❌ Error retrieving bot status: {e}")
383+
384+
258385
@admin_only
259386
async def remove_server(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
260387
"""Remove a VPN server."""
@@ -434,13 +561,4 @@ async def notify_admin_new_user(context: ContextTypes.DEFAULT_TYPE, user: User)
434561
logger.error(f"Failed to notify admin about new user {user.id}: {e}")
435562

436563

437-
# Conversation handler for adding servers
438-
add_server_conv_handler = ConversationHandler(
439-
entry_points=[], # Will be set in main.py
440-
states={
441-
WAITING_SERVER_NAME: [],
442-
WAITING_API_URL: [],
443-
WAITING_CERT_SHA256: [],
444-
},
445-
fallbacks=[],
446-
)
564+
# Note: Conversation handler is properly configured in main.py

bot/main.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
approve_user,
2222
block_user,
2323
list_servers,
24+
bot_status,
2425
start_add_server,
2526
get_server_name,
2627
get_api_url,
2728
get_cert_sha256,
2829
cancel_add_server,
30+
timeout_add_server,
2931
remove_server,
3032
handle_approval_callback,
3133
WAITING_SERVER_NAME,
@@ -106,7 +108,15 @@ def _setup_handlers(self) -> None:
106108
WAITING_API_URL: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_api_url)],
107109
WAITING_CERT_SHA256: [MessageHandler(filters.TEXT & ~filters.COMMAND, get_cert_sha256)],
108110
},
109-
fallbacks=[CommandHandler("cancel", cancel_add_server)],
111+
fallbacks=[
112+
CommandHandler("cancel", cancel_add_server),
113+
MessageHandler(filters.COMMAND, cancel_add_server) # Cancel on any other command
114+
],
115+
conversation_timeout=300, # 5 minutes timeout
116+
name="add_server_conversation",
117+
persistent=False,
118+
allow_reentry=True,
119+
per_message=False
110120
)
111121

112122
# User conversation handler for creating new keys
@@ -117,7 +127,15 @@ def _setup_handlers(self) -> None:
117127
SELECTING_VALIDITY: [CallbackQueryHandler(handle_validity_selection)],
118128
SELECTING_QUOTA: [CallbackQueryHandler(handle_quota_selection)],
119129
},
120-
fallbacks=[CommandHandler("cancel", cancel_new_key)],
130+
fallbacks=[
131+
CommandHandler("cancel", cancel_new_key),
132+
MessageHandler(filters.COMMAND, cancel_new_key) # Cancel on any other command
133+
],
134+
conversation_timeout=300, # 5 minutes timeout
135+
name="new_key_conversation",
136+
persistent=False,
137+
allow_reentry=True,
138+
per_message=False
121139
)
122140

123141
# Add conversation handlers
@@ -128,6 +146,7 @@ def _setup_handlers(self) -> None:
128146
self.application.add_handler(CommandHandler("approve", approve_user))
129147
self.application.add_handler(CommandHandler("block", block_user))
130148
self.application.add_handler(CommandHandler("servers", list_servers))
149+
self.application.add_handler(CommandHandler("status", bot_status))
131150
self.application.add_handler(CommandHandler("removeserver", remove_server))
132151

133152
# User command handlers

0 commit comments

Comments
 (0)