Skip to content

Commit 75f7e5d

Browse files
committed
fix(markdown): escape access_url to avoid Telegram entity parse error
1 parent 7408bb7 commit 75f7e5d

File tree

7 files changed

+487
-38
lines changed

7 files changed

+487
-38
lines changed

bot/handlers/admin.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
99
from telegram.ext import ContextTypes, ConversationHandler
10+
from telegram.helpers import escape_markdown
1011
from sqlalchemy import select, delete
1112
from sqlalchemy.orm import selectinload
1213

@@ -143,14 +144,18 @@ async def list_servers(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
143144
message = "🖥️ **VPN Servers:**\n\n"
144145
for server in servers:
145146
status = "🟢 Active" if server.is_active else "🔴 Inactive"
147+
escaped_name = escape_markdown(server.name, version=2)
148+
escaped_api_url = escape_markdown(server.api_url, version=2)
149+
escaped_cert = escape_markdown(server.cert_sha256[:16] + "...", version=2)
150+
escaped_created = escape_markdown(server.created_at.strftime('%Y-%m-%d %H:%M'), version=2)
146151
message += f"**ID:** {server.id}\n"
147-
message += f"**Name:** {server.name}\n"
152+
message += f"**Name:** {escaped_name}\n"
148153
message += f"**Status:** {status}\n"
149-
message += f"**API URL:** `{server.api_url}`\n"
150-
message += f"**Cert SHA256:** `{server.cert_sha256[:16]}...`\n"
151-
message += f"**Created:** {server.created_at.strftime('%Y-%m-%d %H:%M')}\n\n"
154+
message += f"**API URL:** `{escaped_api_url}`\n"
155+
message += f"**Cert SHA256:** `{escaped_cert}`\n"
156+
message += f"**Created:** {escaped_created}\n\n"
152157

153-
await update.message.reply_text(message, parse_mode='Markdown')
158+
await update.message.reply_text(message, parse_mode='MarkdownV2')
154159

155160

156161
@admin_only
@@ -225,13 +230,17 @@ async def get_cert_sha256(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
225230
f"Added server {server.name}"
226231
)
227232

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+
228237
await update.message.reply_text(
229-
f"✅ Server added successfully!\n\n"
230-
f"**Name:** {server.name}\n"
238+
f"✅ Server added successfully\\!\n\n"
239+
f"**Name:** {escaped_name}\n"
231240
f"**ID:** {server.id}\n"
232-
f"**API URL:** {server.api_url}\n"
233-
f"**Cert SHA256:** {cert_sha256[:16]}...",
234-
parse_mode='Markdown'
241+
f"**API URL:** {escaped_api_url}\n"
242+
f"**Cert SHA256:** {escaped_cert}",
243+
parse_mode='MarkdownV2'
235244
)
236245

237246
# Clear conversation data
@@ -384,9 +393,24 @@ async def handle_approval_callback(update: Update, context: ContextTypes.DEFAULT
384393

385394
async def notify_admin_new_user(context: ContextTypes.DEFAULT_TYPE, user: User) -> None:
386395
"""Notify admin about a new user registration."""
396+
# Format user info with escaping for MarkdownV2
397+
username_part = f"@{user.username}" if user.username else "No username"
398+
name_parts = []
399+
if user.first_name:
400+
name_parts.append(user.first_name)
401+
if user.last_name:
402+
name_parts.append(user.last_name)
403+
name = " ".join(name_parts) if name_parts else "No name"
404+
405+
escaped_username = escape_markdown(username_part, version=2)
406+
escaped_name = escape_markdown(name, version=2)
407+
escaped_role = escape_markdown(user.role.value, version=2)
408+
409+
user_info = f"ID: {user.id}\nUsername: {escaped_username}\nName: {escaped_name}\nRole: {escaped_role}"
410+
387411
message = (
388412
f"👤 **New User Registration**\n\n"
389-
f"{format_user_info(user)}\n\n"
413+
f"{user_info}\n\n"
390414
f"Please choose an action:"
391415
)
392416

@@ -402,7 +426,7 @@ async def notify_admin_new_user(context: ContextTypes.DEFAULT_TYPE, user: User)
402426
await context.bot.send_message(
403427
config.ADMIN_TG_ID,
404428
message,
405-
parse_mode='Markdown',
429+
parse_mode='MarkdownV2',
406430
reply_markup=reply_markup
407431
)
408432
logger.info(f"Notified admin about new user {user.id}")

bot/handlers/sales.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
88
from telegram.ext import ContextTypes, ConversationHandler
9+
from telegram.helpers import escape_markdown
910
from sqlalchemy import select
1011
from sqlalchemy.orm import selectinload
1112

@@ -235,23 +236,24 @@ async def handle_quota_selection(update: Update, context: ContextTypes.DEFAULT_T
235236
validity_display = "1 Week" if validity == ValidityPeriod.WEEK.value else "1 Month"
236237

237238
# Send success message with connection details
239+
escaped_access_url = escape_markdown(access_url, version=2)
238240
message = (
239-
f"🎉 **VPN Connection Created Successfully!**\n\n"
240-
f"🖥️ **Server:** {server.name}\n"
241-
f"⏰ **Validity:** {validity_display}\n"
242-
f"📊 **Traffic Quota:** {quota}\n"
243-
f"⏰ **Expires:** {expires_str}\n\n"
241+
f"🎉 **VPN Connection Created Successfully\\!**\n\n"
242+
f"🖥️ **Server:** {escape_markdown(server.name, version=2)}\n"
243+
f"⏰ **Validity:** {escape_markdown(validity_display, version=2)}\n"
244+
f"📊 **Traffic Quota:** {escape_markdown(quota, version=2)}\n"
245+
f"⏰ **Expires:** {escape_markdown(expires_str, version=2)}\n\n"
244246
f"🔗 **Connection URL:**\n"
245-
f"`{access_url}`\n\n"
247+
f"`{escaped_access_url}`\n\n"
246248
f"📱 **Setup Instructions:**\n"
247-
f"1. Install Outline app on your device\n"
248-
f"2. Copy the connection URL above\n"
249-
f"3. Paste it in the Outline app\n"
250-
f"4. Start using your VPN!\n\n"
249+
f"1\\. Install Outline app on your device\n"
250+
f"2\\. Copy the connection URL above\n"
251+
f"3\\. Paste it in the Outline app\n"
252+
f"4\\. Start using your VPN\\!\n\n"
251253
f"📋 Use /mykeys to view all your connections"
252254
)
253255

254-
await query.edit_message_text(message, parse_mode='Markdown')
256+
await query.edit_message_text(message, parse_mode='MarkdownV2')
255257

256258
except OutlineAPIError as e:
257259
logger.error(f"Failed to create VPN key: {e}")
@@ -310,20 +312,27 @@ async def my_keys(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
310312
message += "🟢 **Active Connections:**\n"
311313
for key in active_keys:
312314
expires_str = key.expires_at.strftime("%Y-%m-%d %H:%M")
315+
escaped_server_name = escape_markdown(key.server.name, version=2)
316+
escaped_quota = escape_markdown(key.traffic_quota, version=2)
317+
escaped_expires = escape_markdown(expires_str, version=2)
318+
escaped_url_preview = escape_markdown(key.access_url[:50] + "...", version=2)
313319
message += (
314-
f"• **{key.server.name}** ({key.traffic_quota})\n"
315-
f" 📅 Expires: {expires_str} UTC\n"
316-
f" 🔗 `{key.access_url[:50]}...`\n\n"
320+
f"• **{escaped_server_name}** \\({escaped_quota}\\)\n"
321+
f" 📅 Expires: {escaped_expires} UTC\n"
322+
f" 🔗 `{escaped_url_preview}`\n\n"
317323
)
318324

319325
# Show expired keys
320326
if expired_keys:
321327
message += "🔴 **Expired Connections:**\n"
322328
for key in expired_keys:
323329
expires_str = key.expires_at.strftime("%Y-%m-%d %H:%M")
330+
escaped_server_name = escape_markdown(key.server.name, version=2)
331+
escaped_quota = escape_markdown(key.traffic_quota, version=2)
332+
escaped_expires = escape_markdown(expires_str, version=2)
324333
message += (
325-
f"• **{key.server.name}** ({key.traffic_quota})\n"
326-
f" 📅 Expired: {expires_str} UTC\n\n"
334+
f"• **{escaped_server_name}** \\({escaped_quota}\\)\n"
335+
f" 📅 Expired: {escaped_expires} UTC\n\n"
327336
)
328337

329338
# Show disabled keys
@@ -332,15 +341,19 @@ async def my_keys(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
332341
for key in disabled_keys:
333342
disabled_str = key.disabled_at.strftime("%Y-%m-%d %H:%M")
334343
reason = key.disabled_reason or "Unknown"
344+
escaped_server_name = escape_markdown(key.server.name, version=2)
345+
escaped_quota = escape_markdown(key.traffic_quota, version=2)
346+
escaped_disabled = escape_markdown(disabled_str, version=2)
347+
escaped_reason = escape_markdown(reason, version=2)
335348
message += (
336-
f"• **{key.server.name}** ({key.traffic_quota})\n"
337-
f" 🚫 Disabled: {disabled_str} UTC\n"
338-
f" 📝 Reason: {reason}\n\n"
349+
f"• **{escaped_server_name}** \\({escaped_quota}\\)\n"
350+
f" 🚫 Disabled: {escaped_disabled} UTC\n"
351+
f" 📝 Reason: {escaped_reason}\n\n"
339352
)
340353

341354
message += f"📊 **Summary:** {len(active_keys)} active, {len(expired_keys)} expired, {len(disabled_keys)} disabled"
342355

343-
await update.message.reply_text(message, parse_mode='Markdown')
356+
await update.message.reply_text(message, parse_mode='MarkdownV2')
344357

345358

346359
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:

bot/scheduler.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from sqlalchemy import select
1010
from sqlalchemy.orm import selectinload
1111
from telegram.ext import Application
12+
from telegram.helpers import escape_markdown
1213

1314
from .database import get_db_session
1415
from .models import AccessKey
@@ -142,18 +143,23 @@ async def _notify_user_key_expired(self, key: AccessKey) -> None:
142143
key: Expired access key
143144
"""
144145
try:
146+
expires_str = key.expires_at.strftime('%Y-%m-%d %H:%M UTC')
147+
escaped_server_name = escape_markdown(key.server.name, version=2)
148+
escaped_quota = escape_markdown(key.traffic_quota, version=2)
149+
escaped_expires = escape_markdown(expires_str, version=2)
150+
145151
message = (
146152
f"⏰ **VPN Connection Expired**\n\n"
147-
f"Your VPN connection to **{key.server.name}** has expired and been disabled.\n\n"
148-
f"📊 **Traffic Quota:** {key.traffic_quota}\n"
149-
f"📅 **Expired on:** {key.expires_at.strftime('%Y-%m-%d %H:%M UTC')}\n\n"
153+
f"Your VPN connection to **{escaped_server_name}** has expired and been disabled\\.\n\n"
154+
f"📊 **Traffic Quota:** {escaped_quota}\n"
155+
f"📅 **Expired on:** {escaped_expires}\n\n"
150156
f"🔑 Use /new to create a new VPN connection"
151157
)
152158

153159
await self.telegram_app.bot.send_message(
154160
key.owner_id,
155161
message,
156-
parse_mode='Markdown'
162+
parse_mode='MarkdownV2'
157163
)
158164

159165
logger.info(f"Notified user {key.owner_id} about expired key {key.id}")

0 commit comments

Comments
 (0)