Files
OutFleet/telegram_bot/bot.py

1871 lines
84 KiB
Python
Raw Normal View History

2025-08-15 04:02:22 +03:00
import logging
import threading
import time
import asyncio
import os
import fcntl
from typing import Optional
from telegram import Update, Bot
2025-08-15 16:33:23 +03:00
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
2025-08-15 04:02:22 +03:00
from django.utils import timezone
from django.conf import settings
from asgiref.sync import sync_to_async
from .models import BotSettings, TelegramMessage, AccessRequest
from .localization import get_localized_message, get_localized_button, get_user_language, MessageLocalizer
logger = logging.getLogger(__name__)
class TelegramBotManager:
"""Singleton manager for Telegram bot with file locking"""
_instance = None
_lock = threading.Lock()
_bot_thread: Optional[threading.Thread] = None
_application: Optional[Application] = None
_stop_event = threading.Event()
_lockfile = None
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# Initialize only once
if not hasattr(self, '_initialized'):
self._initialized = True
self._running = False
self._lockfile = None
# Sync status on startup
self._sync_status_on_startup()
def _acquire_lock(self):
"""Acquire file lock to prevent multiple bot instances"""
try:
# Create lock file path
lock_dir = os.path.join(getattr(settings, 'BASE_DIR', '/tmp'), 'telegram_bot_locks')
os.makedirs(lock_dir, exist_ok=True)
lock_path = os.path.join(lock_dir, 'telegram_bot.lock')
# Open lock file
self._lockfile = open(lock_path, 'w')
# Try to acquire exclusive lock (non-blocking)
fcntl.flock(self._lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
# Write PID to lock file
self._lockfile.write(f"{os.getpid()}\n")
self._lockfile.flush()
logger.info(f"Acquired bot lock: {lock_path}")
return True
except (OSError, IOError) as e:
if self._lockfile:
try:
self._lockfile.close()
except:
pass
self._lockfile = None
logger.warning(f"Could not acquire bot lock: {e}")
return False
def _release_lock(self):
"""Release file lock"""
if self._lockfile:
try:
fcntl.flock(self._lockfile.fileno(), fcntl.LOCK_UN)
self._lockfile.close()
logger.info("Released bot lock")
except:
pass
finally:
self._lockfile = None
def start(self):
"""Start the bot in a background thread"""
if self._running:
logger.warning("Bot is already running")
return
# Try to acquire lock first
if not self._acquire_lock():
raise Exception("Another bot instance is already running (could not acquire lock)")
# No database status to reset - only lock file matters
bot_settings = BotSettings.get_settings()
if not bot_settings.enabled:
self._release_lock()
raise Exception("Bot is disabled in settings")
if not bot_settings.bot_token:
self._release_lock()
raise Exception("Bot token is not configured")
try:
self._stop_event.clear()
self._bot_thread = threading.Thread(target=self._run_bot, daemon=True)
self._bot_thread.start()
# Wait a moment to see if thread starts successfully
time.sleep(0.5)
# Check if thread started successfully
if self._bot_thread.is_alive():
logger.info("Bot started successfully")
else:
self._release_lock()
raise Exception("Bot thread failed to start")
except Exception as e:
self._release_lock()
raise e
2025-08-15 16:33:23 +03:00
async def _is_telegram_admin(self, telegram_user):
"""Check if user is a Telegram admin"""
try:
from django.db.models import Q
bot_settings = await sync_to_async(BotSettings.get_settings)()
# Check by telegram_user_id first
if hasattr(telegram_user, 'id'):
telegram_user_id = telegram_user.id
telegram_username = telegram_user.username if hasattr(telegram_user, 'username') else None
else:
# If just an ID was passed
telegram_user_id = telegram_user
telegram_username = None
# Check if admin by telegram_user_id
admin_by_id = await sync_to_async(
bot_settings.telegram_admins.filter(telegram_user_id=telegram_user_id).exists
)()
if admin_by_id:
return True
# Also check by telegram_username if available
if telegram_username:
admin_by_username = await sync_to_async(
bot_settings.telegram_admins.filter(telegram_username=telegram_username).exists
)()
if admin_by_username:
# Update the user's telegram_user_id for future checks
admin_user = await sync_to_async(
bot_settings.telegram_admins.filter(telegram_username=telegram_username).first
)()
if admin_user and not admin_user.telegram_user_id:
admin_user.telegram_user_id = telegram_user_id
await sync_to_async(admin_user.save)()
logger.info(f"Linked telegram_user_id {telegram_user_id} to admin {admin_user.username}")
return True
return False
except Exception as e:
logger.error(f"Error checking admin status: {e}")
return False
async def _notify_admins_new_request(self, access_request):
"""Notify all Telegram admins about new access request"""
try:
bot_settings = await sync_to_async(BotSettings.get_settings)()
admins = await sync_to_async(list)(bot_settings.telegram_admins.all())
if not admins:
logger.info("No Telegram admins configured, skipping notification")
return
# Prepare user info
user_info = access_request.display_name
telegram_info = f"@{access_request.telegram_username}" if access_request.telegram_username else f"ID: {access_request.telegram_user_id}"
date = access_request.created_at.strftime("%Y-%m-%d %H:%M")
message = access_request.message_text[:200] + "..." if len(access_request.message_text) > 200 else access_request.message_text
# Send notification to each admin
for admin in admins:
try:
if admin.telegram_user_id:
# Get admin language (default to English if not set)
admin_language = 'ru' if hasattr(admin, 'telegram_user_language') else 'en'
notification_text = MessageLocalizer.get_message(
'admin_new_request_notification',
admin_language,
user_info=user_info,
telegram_info=telegram_info,
date=date,
message=message
)
await self._send_notification_to_admin(admin.telegram_user_id, notification_text)
logger.info(f"Sent new request notification to admin {admin.username}")
except Exception as e:
logger.error(f"Failed to notify admin {admin.username}: {e}")
except Exception as e:
logger.error(f"Error notifying admins about new request: {e}")
async def _send_notification_to_admin(self, telegram_user_id, message_text):
"""Send notification message to specific admin"""
try:
bot_settings = await sync_to_async(BotSettings.get_settings)()
if not bot_settings.enabled or not bot_settings.bot_token:
logger.warning("Bot not configured, skipping admin notification")
return
# Create a simple Bot instance for sending notification
from telegram import Bot
from telegram.request import HTTPXRequest
request_kwargs = {
'connection_pool_size': 1,
'read_timeout': bot_settings.connection_timeout,
'write_timeout': bot_settings.connection_timeout,
'connect_timeout': bot_settings.connection_timeout,
}
if bot_settings.use_proxy and bot_settings.proxy_url:
request_kwargs['proxy'] = bot_settings.proxy_url
request = HTTPXRequest(**request_kwargs)
bot = Bot(token=bot_settings.bot_token, request=request)
await bot.send_message(
chat_id=telegram_user_id,
text=message_text,
parse_mode='Markdown'
)
# Clean up bot connection
try:
await request.shutdown()
except:
pass
except Exception as e:
logger.error(f"Failed to send notification to admin {telegram_user_id}: {e}")
async def _create_main_keyboard(self, telegram_user):
"""Create main keyboard for existing users, with admin buttons if user is admin"""
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Get basic buttons
access_button = get_localized_button(telegram_user, 'access')
guide_button = get_localized_button(telegram_user, 'guide')
# Check if user is admin
is_admin = await self._is_telegram_admin(telegram_user)
if is_admin:
# Admin keyboard with additional admin button
access_requests_button = get_localized_button(telegram_user, 'access_requests')
keyboard = [
[KeyboardButton(access_button), KeyboardButton(guide_button)],
[KeyboardButton(access_requests_button)]
]
else:
# Regular user keyboard
keyboard = [
[KeyboardButton(access_button), KeyboardButton(guide_button)]
]
return ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
async def _handle_access_requests_command(self, update: Update):
"""Handle access requests button - show pending requests to admin"""
try:
# Get pending access requests
pending_requests = await sync_to_async(list)(
AccessRequest.objects.filter(approved=False).order_by('-created_at')
)
if not pending_requests:
# No pending requests
no_requests_msg = get_localized_message(update.message.from_user, 'admin_no_pending_requests')
reply_markup = await self._create_main_keyboard(update.message.from_user)
sent_message = await update.message.reply_text(
no_requests_msg,
reply_markup=reply_markup
)
await self._save_outgoing_message(sent_message, update.message.from_user)
return
# Show pending requests with inline keyboards for approve/reject
title_msg = get_localized_message(update.message.from_user, 'admin_access_requests_title')
# Send title message with main keyboard
reply_markup = await self._create_main_keyboard(update.message.from_user)
title_sent = await update.message.reply_text(
title_msg,
reply_markup=reply_markup,
parse_mode='Markdown'
)
await self._save_outgoing_message(title_sent, update.message.from_user)
# Send each request with inline keyboard for actions
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
for request in pending_requests[:5]: # Show max 5 requests
# Format request info
user_info = request.display_name
date = request.created_at.strftime("%Y-%m-%d %H:%M")
message_preview = request.message_text[:100] + "..." if len(request.message_text) > 100 else request.message_text
request_text = get_localized_message(
update.message.from_user,
'admin_request_item',
user_info=user_info,
date=date,
message_preview=message_preview
)
# Create inline keyboard for this request
approve_btn_text = get_localized_button(update.message.from_user, 'approve')
reject_btn_text = get_localized_button(update.message.from_user, 'reject')
details_btn_text = get_localized_button(update.message.from_user, 'details')
inline_keyboard = [
[
InlineKeyboardButton(approve_btn_text, callback_data=f"approve_{request.id}"),
InlineKeyboardButton(reject_btn_text, callback_data=f"reject_{request.id}")
],
[InlineKeyboardButton(details_btn_text, callback_data=f"details_{request.id}")]
]
inline_markup = InlineKeyboardMarkup(inline_keyboard)
request_sent = await update.message.reply_text(
request_text,
reply_markup=inline_markup,
parse_mode='Markdown'
)
await self._save_outgoing_message(request_sent, update.message.from_user)
if len(pending_requests) > 5:
remaining_msg = f"... и еще {len(pending_requests) - 5} заявок" if get_user_language(update.message.from_user) == 'ru' else f"... and {len(pending_requests) - 5} more requests"
await update.message.reply_text(remaining_msg)
logger.info(f"Showed {len(pending_requests)} pending requests to admin {update.message.from_user.username}")
except Exception as e:
logger.error(f"Error handling access requests command: {e}")
error_msg = get_localized_message(update.message.from_user, 'admin_error_processing', error=str(e))
await update.message.reply_text(error_msg)
async def _handle_callback_query(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle inline keyboard button presses (callback queries)"""
try:
query = update.callback_query
await query.answer() # Acknowledge the callback
# Check if user is admin
if not await self._is_telegram_admin(query.from_user):
await query.edit_message_text("❌ Access denied. Admin rights required.")
return
# Parse callback data
callback_data = query.data
action, request_id = callback_data.split('_', 1)
if action == "approve":
await self._handle_approve_callback(query, request_id)
elif action == "reject":
await self._handle_reject_callback(query, request_id)
elif action == "details":
await self._handle_details_callback(query, request_id)
elif action == "sg": # Subscription group selection
await self._handle_subscription_group_callback(query, callback_data)
elif action == "confirm": # Confirm approval with selected groups
await self._handle_confirm_approval_callback(query, request_id)
elif action == "cancel": # Cancel approval process
await self._handle_cancel_callback(query, request_id)
else:
await query.edit_message_text("❌ Unknown action")
except Exception as e:
logger.error(f"Error handling callback query: {e}")
try:
await query.edit_message_text(f"❌ Error: {str(e)}")
except:
pass
async def _handle_approve_callback(self, query, request_id):
"""Handle approve button press - show subscription groups selection"""
try:
# Get the request
request = await sync_to_async(AccessRequest.objects.get)(id=request_id)
# Check if already processed
if request.approved:
already_processed_msg = get_localized_message(query.from_user, 'admin_request_already_processed')
await query.edit_message_text(already_processed_msg)
return
# Get available subscription groups
from vpn.models_xray import SubscriptionGroup
groups = await sync_to_async(list)(
SubscriptionGroup.objects.filter(is_active=True).order_by('name')
)
if not groups:
await query.edit_message_text("❌ No subscription groups available")
return
# Create inline keyboard with subscription groups
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
user_info = request.display_name
choose_groups_msg = get_localized_message(
query.from_user,
'admin_choose_subscription_groups',
user_info=user_info
)
# Create buttons for each group (max 2 per row)
keyboard = []
for i in range(0, len(groups), 2):
row = []
for j in range(2):
if i + j < len(groups):
group = groups[i + j]
button_text = f"{group.name}"
callback_data = f"sg_toggle_{group.id}_{request_id}"
row.append(InlineKeyboardButton(button_text, callback_data=callback_data))
keyboard.append(row)
# Add confirm and cancel buttons
confirm_btn_text = get_localized_button(query.from_user, 'confirm_approval')
cancel_btn_text = get_localized_button(query.from_user, 'cancel')
keyboard.append([
InlineKeyboardButton(confirm_btn_text, callback_data=f"confirm_{request_id}"),
InlineKeyboardButton(cancel_btn_text, callback_data=f"cancel_{request_id}")
])
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text(choose_groups_msg, reply_markup=reply_markup, parse_mode='Markdown')
except Exception as e:
logger.error(f"Error handling approve callback: {e}")
error_msg = get_localized_message(query.from_user, 'admin_error_processing', error=str(e))
await query.edit_message_text(error_msg)
async def _handle_subscription_group_callback(self, query, callback_data):
"""Handle subscription group toggle button"""
try:
# Parse callback data: sg_toggle_{group_id}_{request_id}
parts = callback_data.split('_')
group_id = parts[2]
request_id = parts[3]
# Get current message text and keyboard
current_text = query.message.text
current_keyboard = query.message.reply_markup.inline_keyboard
# Toggle the group selection
updated_keyboard = []
for row in current_keyboard:
updated_row = []
for button in row:
if button.callback_data and button.callback_data.startswith(f"sg_toggle_{group_id}_"):
# Toggle this button
if button.text.startswith(""):
# Select it
new_text = button.text.replace("", "")
else:
# Deselect it
new_text = button.text.replace("", "")
from telegram import InlineKeyboardButton
updated_row.append(InlineKeyboardButton(new_text, callback_data=button.callback_data))
else:
updated_row.append(button)
updated_keyboard.append(updated_row)
from telegram import InlineKeyboardMarkup
updated_markup = InlineKeyboardMarkup(updated_keyboard)
await query.edit_message_reply_markup(reply_markup=updated_markup)
except Exception as e:
logger.error(f"Error handling subscription group callback: {e}")
async def _handle_confirm_approval_callback(self, query, request_id):
"""Handle confirm approval button - create user and assign groups"""
try:
# Get the request
request = await sync_to_async(AccessRequest.objects.get)(id=request_id)
# Check if already processed
if request.approved:
already_processed_msg = get_localized_message(query.from_user, 'admin_request_already_processed')
await query.edit_message_text(already_processed_msg)
return
# Get selected groups from the keyboard
selected_groups = []
from vpn.models_xray import SubscriptionGroup
for row in query.message.reply_markup.inline_keyboard:
for button in row:
if button.callback_data and button.callback_data.startswith("sg_toggle_") and button.text.startswith(""):
# Extract group_id from callback_data
group_id = button.callback_data.split('_')[2]
group = await sync_to_async(SubscriptionGroup.objects.get)(id=group_id)
selected_groups.append(group)
if not selected_groups:
await query.edit_message_text("❌ Please select at least one subscription group")
return
# Save selected groups to the request
await sync_to_async(request.selected_subscription_groups.set)(selected_groups)
try:
# Create or get user
from vpn.models import User
from vpn.models_xray import UserSubscription
import secrets
import string
# Check if user already exists
existing_user = await sync_to_async(
User.objects.filter(telegram_user_id=request.telegram_user_id).first
)()
if existing_user:
user = existing_user
logger.info(f"Using existing user {user.username} for telegram_user_id {request.telegram_user_id}")
else:
# Check if selected_existing_user is set
if request.selected_existing_user:
# Link telegram to existing user
user = request.selected_existing_user
user.telegram_user_id = request.telegram_user_id
user.telegram_username = request.telegram_username
await sync_to_async(user.save)()
logger.info(f"Linked telegram account to existing user {user.username}")
else:
# Create new user
username = request.desired_username or request.telegram_username or f"tg_{request.telegram_user_id}"
# Ensure unique username
base_username = username
counter = 1
while await sync_to_async(User.objects.filter(username=username).exists)():
username = f"{base_username}_{counter}"
counter += 1
# Generate random password
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for _ in range(16))
# Create user
user = await sync_to_async(User.objects.create_user)(
username=username,
password=password,
telegram_user_id=request.telegram_user_id,
telegram_username=request.telegram_username,
first_name=request.telegram_first_name or '',
last_name=request.telegram_last_name or '',
comment=f"Created from Telegram request #{request.id}"
)
logger.info(f"Created new user {user.username} from Telegram request")
# Link user to request
request.user = user
await sync_to_async(request.save)()
# Assign subscription groups to user
for subscription_group in selected_groups:
user_subscription, created = await sync_to_async(UserSubscription.objects.get_or_create)(
user=user,
subscription_group=subscription_group,
defaults={'active': True}
)
if created:
logger.info(f"Assigned subscription group '{subscription_group.name}' to user {user.username}")
else:
# Ensure it's active if it already existed
if not user_subscription.active:
user_subscription.active = True
await sync_to_async(user_subscription.save)()
logger.info(f"Re-activated subscription group '{subscription_group.name}' for user {user.username}")
# Mark as approved
request.approved = True
await sync_to_async(request.save)()
# Send success message to admin
groups_list = ", ".join([group.name for group in selected_groups])
success_msg = get_localized_message(
query.from_user,
'admin_approval_success',
user_info=request.display_name,
groups=groups_list
)
await query.edit_message_text(success_msg, parse_mode='Markdown')
# Send approval notification to user
# Create a dummy user object for localization
class DummyUser:
def __init__(self, lang):
self.language_code = lang
user_lang = DummyUser(request.user_language if hasattr(request, 'user_language') else 'en')
approval_msg = get_localized_message(user_lang, 'approval_notification')
await self._send_notification_to_admin(request.telegram_user_id, approval_msg)
logger.info(f"Admin {query.from_user.username} approved request {request_id} with {len(selected_groups)} groups")
except Exception as e:
logger.error(f"Error in approval process: {e}")
error_msg = get_localized_message(query.from_user, 'admin_error_processing', error=str(e))
await query.edit_message_text(error_msg)
except Exception as e:
logger.error(f"Error handling confirm approval callback: {e}")
error_msg = get_localized_message(query.from_user, 'admin_error_processing', error=str(e))
await query.edit_message_text(error_msg)
async def _handle_reject_callback(self, query, request_id):
"""Handle reject button press"""
try:
# Get the request
request = await sync_to_async(AccessRequest.objects.get)(id=request_id)
# Check if already processed
if request.approved:
already_processed_msg = get_localized_message(query.from_user, 'admin_request_already_processed')
await query.edit_message_text(already_processed_msg)
return
# Mark as rejected (we can add a rejected field or just delete)
await sync_to_async(request.delete)()
# Send success message to admin
success_msg = get_localized_message(
query.from_user,
'admin_rejection_success',
user_info=request.display_name
)
await query.edit_message_text(success_msg, parse_mode='Markdown')
logger.info(f"Admin {query.from_user.username} rejected request {request_id}")
except Exception as e:
logger.error(f"Error handling reject callback: {e}")
error_msg = get_localized_message(query.from_user, 'admin_error_processing', error=str(e))
await query.edit_message_text(error_msg)
async def _handle_details_callback(self, query, request_id):
"""Handle details button press - show full request info"""
try:
# Get the request
request = await sync_to_async(AccessRequest.objects.get)(id=request_id)
# Format detailed information
details = f"**📋 Request Details**\n\n"
details += f"**👤 User:** {request.display_name}\n"
details += f"**📱 Telegram:** @{request.telegram_username}" if request.telegram_username else f"**📱 Telegram ID:** {request.telegram_user_id}\n"
details += f"**📅 Date:** {request.created_at.strftime('%Y-%m-%d %H:%M:%S')}\n"
details += f"**🆔 Request ID:** {request.id}\n"
details += f"**👤 Desired Username:** {request.desired_username}\n\n"
details += f"**💬 Full Message:**\n{request.message_text}"
# Create back button
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
back_keyboard = [[
InlineKeyboardButton("⬅️ Back", callback_data=f"approve_{request_id}"),
InlineKeyboardButton("❌ Reject", callback_data=f"reject_{request_id}")
]]
reply_markup = InlineKeyboardMarkup(back_keyboard)
await query.edit_message_text(details, reply_markup=reply_markup, parse_mode='Markdown')
except Exception as e:
logger.error(f"Error handling details callback: {e}")
error_msg = get_localized_message(query.from_user, 'admin_error_processing', error=str(e))
await query.edit_message_text(error_msg)
async def _handle_cancel_callback(self, query, request_id):
"""Handle cancel button press - return to original request view"""
try:
# Get the request
request = await sync_to_async(AccessRequest.objects.get)(id=request_id)
# Recreate original request display
user_info = request.display_name
date = request.created_at.strftime("%Y-%m-%d %H:%M")
message_preview = request.message_text[:100] + "..." if len(request.message_text) > 100 else request.message_text
request_text = get_localized_message(
query.from_user,
'admin_request_item',
user_info=user_info,
date=date,
message_preview=message_preview
)
# Create original inline keyboard
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
approve_btn_text = get_localized_button(query.from_user, 'approve')
reject_btn_text = get_localized_button(query.from_user, 'reject')
details_btn_text = get_localized_button(query.from_user, 'details')
inline_keyboard = [
[
InlineKeyboardButton(approve_btn_text, callback_data=f"approve_{request.id}"),
InlineKeyboardButton(reject_btn_text, callback_data=f"reject_{request.id}")
],
[InlineKeyboardButton(details_btn_text, callback_data=f"details_{request.id}")]
]
inline_markup = InlineKeyboardMarkup(inline_keyboard)
await query.edit_message_text(request_text, reply_markup=inline_markup, parse_mode='Markdown')
except Exception as e:
logger.error(f"Error handling cancel callback: {e}")
error_msg = get_localized_message(query.from_user, 'admin_error_processing', error=str(e))
await query.edit_message_text(error_msg)
2025-08-15 04:02:22 +03:00
def stop(self):
"""Stop the bot"""
if not self._running:
logger.warning("Bot is not running")
return
logger.info("Stopping bot...")
self._stop_event.set()
if self._application:
# Stop the application
try:
self._application.stop_running()
except Exception as e:
logger.error(f"Error stopping application: {e}")
# Wait for thread to finish
if self._bot_thread and self._bot_thread.is_alive():
self._bot_thread.join(timeout=10)
self._running = False
# Release file lock
self._release_lock()
logger.info("Bot stopped")
def restart(self):
"""Restart the bot"""
logger.info("Restarting bot...")
self.stop()
time.sleep(2) # Wait a bit before restarting
self.start()
def _run_bot(self):
"""Run the bot (in background thread with asyncio loop)"""
try:
# Create new event loop for this thread
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Run the async bot
loop.run_until_complete(self._async_run_bot())
except Exception as e:
logger.error(f"Bot error: {e}")
self._running = False
# Release lock on error
self._release_lock()
finally:
try:
loop.close()
except:
pass
async def _async_run_bot(self):
"""Async bot runner"""
try:
self._running = True
settings = await sync_to_async(BotSettings.get_settings)()
# Create application with custom request settings
from telegram.request import HTTPXRequest
# Prepare request settings
request_kwargs = {
'connection_pool_size': 8,
'read_timeout': settings.connection_timeout,
'write_timeout': settings.connection_timeout,
'connect_timeout': settings.connection_timeout,
'pool_timeout': settings.connection_timeout
}
# Add proxy if configured
if settings.use_proxy and settings.proxy_url:
logger.info(f"Using proxy: {settings.proxy_url}")
request_kwargs['proxy'] = settings.proxy_url
request = HTTPXRequest(**request_kwargs)
# Create application builder
app_builder = Application.builder().token(settings.bot_token).request(request)
# Use custom API base URL if provided
if settings.api_base_url and settings.api_base_url != "https://api.telegram.org":
logger.info(f"Using custom API base URL: {settings.api_base_url}")
app_builder = app_builder.base_url(settings.api_base_url)
self._application = app_builder.build()
# Add handlers
self._application.add_handler(CommandHandler("start", self._handle_start))
2025-08-15 16:33:23 +03:00
self._application.add_handler(CallbackQueryHandler(self._handle_callback_query))
2025-08-15 04:02:22 +03:00
self._application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message))
self._application.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, self._handle_other))
# Initialize application
await self._application.initialize()
await self._application.start()
await self._application.updater.start_polling(
allowed_updates=Update.ALL_TYPES,
drop_pending_updates=True
)
logger.info("Bot polling started successfully")
# Test connection
try:
logger.info("Testing bot connection...")
bot = self._application.bot
me = await bot.get_me()
logger.info(f"Bot connected successfully. Bot info: @{me.username} ({me.first_name})")
except Exception as test_e:
logger.error(f"Bot connection test failed: {test_e}")
logger.error(f"Connection settings - API URL: {settings.api_base_url}, Timeout: {settings.connection_timeout}s")
if settings.use_proxy:
logger.error(f"Proxy settings - URL: {settings.proxy_url}")
raise
# Keep running until stop event is set
while not self._stop_event.is_set():
await asyncio.sleep(1)
logger.info("Stop event received, shutting down...")
except Exception as e:
logger.error(f"Async bot error: {e}")
raise
finally:
# Clean shutdown
if self._application:
try:
await self._application.updater.stop()
await self._application.stop()
await self._application.shutdown()
except Exception as e:
logger.error(f"Error during shutdown: {e}")
self._running = False
# Release lock on shutdown
self._release_lock()
async def _handle_start(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle /start command"""
try:
# Save incoming message
saved_message = await self._save_message(update.message, 'incoming')
# Check if user exists by telegram_user_id
user_response = await self._check_user_access(update.message.from_user)
if user_response['action'] == 'existing_user':
# User already exists - show keyboard with options
2025-08-15 16:33:23 +03:00
reply_markup = await self._create_main_keyboard(update.message.from_user)
2025-08-15 04:02:22 +03:00
help_text = get_localized_message(update.message.from_user, 'help_text')
sent_message = await update.message.reply_text(
help_text,
reply_markup=reply_markup
)
logger.info(f"Handled /start from existing user {user_response['user'].username}")
elif user_response['action'] == 'show_new_user_keyboard':
# Show keyboard with request access button for new users
await self._show_new_user_keyboard(update)
elif user_response['action'] == 'pending_request':
# Show pending request message
await self._show_pending_request_message(update)
else:
# Fallback case - show new user keyboard
await self._show_new_user_keyboard(update)
# Save outgoing message (if sent_message was created)
if 'sent_message' in locals():
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
except Exception as e:
logger.error(f"Error handling /start: {e}")
async def _handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle text messages"""
try:
# Save incoming message
saved_message = await self._save_message(update.message, 'incoming')
# Check if user exists by telegram_user_id
user_response = await self._check_user_access(update.message.from_user)
if user_response['action'] == 'existing_user':
# Get localized button texts for comparison
access_btn = get_localized_button(update.message.from_user, 'access')
2025-08-15 04:53:30 +03:00
guide_btn = get_localized_button(update.message.from_user, 'guide')
2025-08-15 16:33:23 +03:00
access_requests_btn = get_localized_button(update.message.from_user, 'access_requests')
2025-08-15 04:02:22 +03:00
all_in_one_btn = get_localized_button(update.message.from_user, 'all_in_one')
back_btn = get_localized_button(update.message.from_user, 'back')
group_prefix = get_localized_button(update.message.from_user, 'group_prefix')
# Check if this is a keyboard command
if update.message.text == access_btn:
await self._handle_access_command(update, user_response['user'])
2025-08-15 04:53:30 +03:00
elif update.message.text == guide_btn:
await self._handle_guide_command(update)
2025-08-15 16:33:23 +03:00
elif update.message.text == access_requests_btn:
# Admin command - check permissions and handle
if await self._is_telegram_admin(update.message.from_user):
await self._handle_access_requests_command(update)
else:
await self._send_help_message(update)
2025-08-15 04:02:22 +03:00
elif update.message.text.startswith(group_prefix):
# Handle specific group selection
group_name = update.message.text.replace(group_prefix, "")
await self._handle_group_selection(update, user_response['user'], group_name)
elif update.message.text == all_in_one_btn:
# Handle All-in-one selection
await self._handle_all_in_one(update, user_response['user'])
elif update.message.text == back_btn:
# Handle back button
await self._handle_back_to_main(update, user_response['user'])
2025-08-15 04:53:30 +03:00
elif update.message.text == get_localized_button(update.message.from_user, 'android'):
# Handle Android guide
await self._handle_android_guide(update)
elif update.message.text == get_localized_button(update.message.from_user, 'ios'):
# Handle iOS guide
await self._handle_ios_guide(update)
elif update.message.text == get_localized_button(update.message.from_user, 'web_portal'):
# Handle web portal
await self._handle_web_portal(update, user_response['user'])
2025-08-15 04:02:22 +03:00
else:
# Unrecognized command - send help message
await self._send_help_message(update)
return
elif user_response['action'] == 'show_new_user_keyboard':
# Check if user clicked "Request Access" button
request_access_btn = get_localized_button(update.message.from_user, 'request_access')
if update.message.text == request_access_btn:
# User clicked request access - create request
await self._create_access_request(update.message.from_user, update.message, saved_message)
request_created_msg = get_localized_message(update.message.from_user, 'access_request_created')
sent_message = await update.message.reply_text(request_created_msg)
# Save outgoing message
await self._save_outgoing_message(sent_message, update.message.from_user)
logger.info(f"Created access request for user {update.message.from_user.username or update.message.from_user.id}")
else:
# User sent other text - show keyboard again
await self._show_new_user_keyboard(update)
return
elif user_response['action'] == 'pending_request':
# Show pending request message
await self._show_pending_request_message(update)
return
elif user_response['action'] == 'create_request':
# Create new access request with this message
await self._create_access_request(update.message.from_user, update.message, saved_message)
request_created_msg = get_localized_message(update.message.from_user, 'access_request_created')
sent_message = await update.message.reply_text(
request_created_msg
)
logger.info(f"Created access request for new user {update.message.from_user.username or update.message.from_user.id}")
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
except Exception as e:
logger.error(f"Error handling message: {e}")
async def _handle_other(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Handle non-text messages (photos, documents, etc.)"""
try:
# Save incoming message
await self._save_message(update.message, 'incoming')
# Auto-reply with localized message
message_type = "content"
if update.message.photo:
message_type = "photo"
elif update.message.document:
message_type = "document"
elif update.message.voice:
message_type = "voice"
elif update.message.video:
message_type = "video"
# Get localized content type name
language = get_user_language(update.message.from_user)
localized_type = MessageLocalizer.get_content_type_name(message_type, language)
reply_text = get_localized_message(
update.message.from_user,
'received_content',
message_type=localized_type
)
sent_message = await update.message.reply_text(reply_text)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Handled {message_type} from {update.message.from_user.username or update.message.from_user.id}")
except Exception as e:
logger.error(f"Error handling non-text message: {e}")
async def _save_message(self, message, direction='incoming'):
"""Save message to database"""
try:
# Prepare message text
message_text = message.text or message.caption or ""
# Handle different message types
if message.photo:
message_text = f"[Photo] {message_text}"
elif message.document:
message_text = f"[Document: {message.document.file_name}] {message_text}"
elif message.voice:
message_text = f"[Voice message] {message_text}"
elif message.video:
message_text = f"[Video] {message_text}"
elif message.sticker:
message_text = f"[Sticker: {message.sticker.emoji or 'no emoji'}]"
# Convert message to dict for raw_data
raw_data = message.to_dict() if hasattr(message, 'to_dict') else {}
# Get user language and create database record
user_language = get_user_language(message.from_user)
telegram_message = await sync_to_async(TelegramMessage.objects.create)(
direction=direction,
telegram_user_id=message.from_user.id,
telegram_username=message.from_user.username or "",
telegram_first_name=message.from_user.first_name or "",
telegram_last_name=message.from_user.last_name or "",
chat_id=message.chat_id,
message_id=message.message_id,
message_text=message_text,
raw_data=raw_data,
user_language=user_language
)
logger.debug(f"Saved {direction} message from {message.from_user.id}")
return telegram_message
except Exception as e:
logger.error(f"Error saving message: {e}")
return None
async def _save_outgoing_message(self, message, to_user):
"""Save outgoing message to database"""
try:
raw_data = message.to_dict() if hasattr(message, 'to_dict') else {}
user_language = get_user_language(to_user)
await sync_to_async(TelegramMessage.objects.create)(
direction='outgoing',
telegram_user_id=to_user.id,
telegram_username=to_user.username or "",
telegram_first_name=to_user.first_name or "",
telegram_last_name=to_user.last_name or "",
chat_id=message.chat_id,
message_id=message.message_id,
message_text=message.text,
raw_data=raw_data,
user_language=user_language
)
logger.debug(f"Saved outgoing message to {to_user.id}")
except Exception as e:
logger.error(f"Error saving outgoing message: {e}")
async def _check_user_access(self, telegram_user):
"""Check if user exists or has pending request, or can be linked by username"""
from vpn.models import User
try:
# Check if user already exists by telegram_user_id
user = await sync_to_async(User.objects.filter(telegram_user_id=telegram_user.id).first)()
if user:
return {'action': 'existing_user', 'user': user}
# Check if user can be linked by telegram username
if telegram_user.username:
# Look for existing user with matching telegram_username (case-insensitive)
existing_user_by_username = await sync_to_async(
User.objects.filter(
telegram_username__iexact=telegram_user.username,
telegram_user_id__isnull=True # Not yet linked to Telegram
).first
)()
if existing_user_by_username:
# Link this telegram account to existing user
await self._link_telegram_to_user(existing_user_by_username, telegram_user)
return {'action': 'existing_user', 'user': existing_user_by_username}
# Check if this telegram_user_id was previously linked to a user but is now unlinked
# This handles the case where an admin "untied" the user from Telegram
unlinked_user = await sync_to_async(
User.objects.filter(
telegram_username__iexact=telegram_user.username if telegram_user.username else '',
telegram_user_id__isnull=True
).first
)() if telegram_user.username else None
# Check if user has pending access request
existing_request = await sync_to_async(
AccessRequest.objects.filter(telegram_user_id=telegram_user.id).first
)()
if existing_request:
if existing_request.approved:
# Check if there was an approved request but user was unlinked
# In this case, allow creating a new request (treat as new user)
if unlinked_user:
# Delete old request since user was unlinked
await sync_to_async(existing_request.delete)()
logger.info(f"Deleted old approved request for unlinked user @{telegram_user.username}")
return {'action': 'show_new_user_keyboard'}
else:
# Request approved but user not created yet (shouldn't happen but handle gracefully)
return {'action': 'show_new_user_keyboard'}
else:
# Check if user was unlinked after making the request
if unlinked_user:
# Delete old pending request and allow new one
await sync_to_async(existing_request.delete)()
logger.info(f"Deleted old pending request for unlinked user @{telegram_user.username}")
return {'action': 'show_new_user_keyboard'}
else:
# Request pending
return {'action': 'pending_request'}
# No user and no request - new user
return {'action': 'show_new_user_keyboard'}
except Exception as e:
logger.error(f"Error checking user access: {e}")
# Default to new user keyboard if check fails
return {'action': 'show_new_user_keyboard'}
async def _handle_access_command(self, update: Update, user):
"""Handle Access button - show subscription groups keyboard"""
try:
# Import Xray models
from vpn.models_xray import UserSubscription
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Get user's active subscription groups
subscriptions = await sync_to_async(
lambda: list(
UserSubscription.objects.filter(
user=user,
active=True,
subscription_group__is_active=True
).select_related('subscription_group')
)
)()
if subscriptions:
# Create keyboard with subscription groups using localized buttons
keyboard = []
# Get localized button texts
all_in_one_btn = get_localized_button(update.message.from_user, 'all_in_one')
group_prefix = get_localized_button(update.message.from_user, 'group_prefix')
back_btn = get_localized_button(update.message.from_user, 'back')
# Add All-in-one button first
keyboard.append([KeyboardButton(all_in_one_btn)])
# Add individual group buttons in 2 columns
group_buttons = []
for sub in subscriptions:
group_name = sub.subscription_group.name
group_buttons.append(KeyboardButton(f"{group_prefix}{group_name}"))
# Arrange buttons in 2 columns
for i in range(0, len(group_buttons), 2):
if i + 1 < len(group_buttons):
# Two buttons in a row
keyboard.append([group_buttons[i], group_buttons[i + 1]])
else:
# One button in the last row
keyboard.append([group_buttons[i]])
# Add back button
keyboard.append([KeyboardButton(back_btn)])
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
# Send message with keyboard using localized text
choose_text = get_localized_message(update.message.from_user, 'choose_subscription')
all_in_one_desc = get_localized_message(update.message.from_user, 'all_in_one_desc')
group_desc = get_localized_message(update.message.from_user, 'group_desc')
select_option = get_localized_message(update.message.from_user, 'select_option')
message_text = f"{choose_text}\n\n"
message_text += f"{all_in_one_desc}\n"
message_text += f"{group_desc}\n\n"
message_text += select_option
sent_message = await update.message.reply_text(
message_text,
reply_markup=reply_markup,
parse_mode='Markdown'
)
else:
# No active subscriptions - show main keyboard
2025-08-15 16:33:23 +03:00
reply_markup = await self._create_main_keyboard(update.message.from_user)
2025-08-15 04:02:22 +03:00
no_subs_msg = get_localized_message(update.message.from_user, 'no_subscriptions')
sent_message = await update.message.reply_text(
no_subs_msg,
reply_markup=reply_markup
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Showed subscription groups keyboard to user {user.username}")
except Exception as e:
logger.error(f"Error handling Access command: {e}")
error_msg = get_localized_message(update.message.from_user, 'error_loading_subscriptions')
await update.message.reply_text(error_msg)
async def _create_access_request(self, telegram_user, message, saved_message=None):
"""Create new access request"""
try:
# Check if request already exists (safety check)
existing = await sync_to_async(
AccessRequest.objects.filter(telegram_user_id=telegram_user.id).first
)()
if existing and not existing.approved:
logger.warning(f"Access request already exists for user {telegram_user.id}")
return existing
# Create new request with default username from Telegram and user language
default_username = telegram_user.username or ""
user_language = get_user_language(telegram_user)
request = await sync_to_async(AccessRequest.objects.create)(
telegram_user_id=telegram_user.id,
telegram_username=telegram_user.username or "",
telegram_first_name=telegram_user.first_name or "",
telegram_last_name=telegram_user.last_name or "",
message_text=message.text or message.caption or "[Non-text message]",
chat_id=message.chat_id,
first_message=saved_message,
desired_username=default_username,
user_language=user_language
)
logger.info(f"Created access request for user {telegram_user.username or telegram_user.id}")
2025-08-15 16:33:23 +03:00
# Notify admins about new request
await self._notify_admins_new_request(request)
2025-08-15 04:02:22 +03:00
return request
except Exception as e:
logger.error(f"Error creating access request: {e}")
return None
async def _send_help_message(self, update: Update):
"""Send help message for unrecognized commands"""
try:
# Create main keyboard for existing users with localized buttons
2025-08-15 16:33:23 +03:00
reply_markup = await self._create_main_keyboard(update.message.from_user)
2025-08-15 04:02:22 +03:00
help_text = get_localized_message(update.message.from_user, 'help_text')
sent_message = await update.message.reply_text(
help_text,
reply_markup=reply_markup
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
except Exception as e:
logger.error(f"Error sending help message: {e}")
async def _handle_group_selection(self, update: Update, user, group_name):
"""Handle specific group selection"""
try:
from django.conf import settings
# Get the base URL for subscription links from settings
base_url = getattr(settings, 'EXTERNAL_ADDRESS', 'https://your-server.com')
# Parse base_url to ensure it has https:// scheme
if not base_url.startswith(('http://', 'https://')):
base_url = f"https://{base_url}"
# Generate group-specific subscription link
subscription_link = f"{base_url}/xray/{user.hash}?group={group_name}"
# Also generate user portal link
portal_link = f"{base_url}/u/{user.hash}"
2025-08-15 04:53:30 +03:00
# Get server information for this group
from vpn.models_xray import SubscriptionGroup, ServerInbound
group_servers_info = ""
try:
# Find the subscription group by name
subscription_group = await sync_to_async(
SubscriptionGroup.objects.filter(
name=group_name,
is_active=True
).prefetch_related('inbounds').first
)()
if subscription_group:
# Get servers where this group's inbounds are deployed
deployed_servers = await sync_to_async(
lambda: list(
ServerInbound.objects.filter(
inbound__in=subscription_group.inbounds.all(),
active=True
).select_related('server', 'inbound').values_list(
'server__name', 'inbound__protocol'
).distinct()
)
)()
if deployed_servers:
# Group by server
servers_data = {}
for server_name, protocol in deployed_servers:
if server_name not in servers_data:
servers_data[server_name] = []
if protocol.upper() not in servers_data[server_name]:
servers_data[server_name].append(protocol.upper())
# Build server info text
if servers_data:
servers_header = get_localized_message(update.message.from_user, 'servers_in_group')
group_servers_info = f"\n{servers_header}\n"
for server_name, protocols in servers_data.items():
protocols_str = ", ".join(protocols)
group_servers_info += f"{server_name} ({protocols_str})\n"
group_servers_info += "\n"
except Exception as e:
logger.warning(f"Could not get server info for group {group_name}: {e}")
2025-08-15 04:02:22 +03:00
# Build localized message
group_title = get_localized_message(update.message.from_user, 'group_subscription', group_name=group_name)
sub_link_label = get_localized_message(update.message.from_user, 'subscription_link')
portal_label = get_localized_message(update.message.from_user, 'web_portal')
tap_note = get_localized_message(update.message.from_user, 'tap_to_copy')
2025-08-15 04:53:30 +03:00
message_text = f"{group_title}\n"
message_text += group_servers_info # Add server info
2025-08-15 04:02:22 +03:00
message_text += f"{sub_link_label}\n"
message_text += f"`{subscription_link}`\n\n"
message_text += tap_note
# Create back navigation keyboard with only back button
from telegram import ReplyKeyboardMarkup, KeyboardButton
back_btn = get_localized_button(update.message.from_user, 'back')
keyboard = [
[KeyboardButton(back_btn)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
sent_message = await update.message.reply_text(
message_text,
reply_markup=reply_markup,
parse_mode='Markdown'
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Sent group {group_name} subscription to user {user.username}")
except Exception as e:
logger.error(f"Error handling group selection: {e}")
error_msg = get_localized_message(update.message.from_user, 'error_loading_group')
await update.message.reply_text(error_msg)
async def _handle_all_in_one(self, update: Update, user):
"""Handle All-in-one selection"""
try:
# Import Xray models
from vpn.models_xray import UserSubscription, ServerInbound
from django.conf import settings
# Get user's active subscription groups
subscriptions = await sync_to_async(
lambda: list(
UserSubscription.objects.filter(
user=user,
active=True,
subscription_group__is_active=True
).select_related('subscription_group').prefetch_related('subscription_group__inbounds')
)
)()
if subscriptions:
# Get the base URL for subscription links from settings
base_url = getattr(settings, 'EXTERNAL_ADDRESS', 'https://your-server.com')
# Parse base_url to ensure it has https:// scheme
if not base_url.startswith(('http://', 'https://')):
base_url = f"https://{base_url}"
# Generate universal subscription link
subscription_link = f"{base_url}/xray/{user.hash}"
# Also generate user portal link
portal_link = f"{base_url}/u/{user.hash}"
# Build message with detailed server/subscription list using localized text
all_in_one_title = get_localized_message(update.message.from_user, 'all_in_one_subscription')
access_includes = get_localized_message(update.message.from_user, 'your_access_includes')
message_text = f"{all_in_one_title}\n\n"
message_text += f"{access_includes}\n\n"
# Collect all servers and their subscriptions
servers_data = {}
for sub in subscriptions:
group = sub.subscription_group
group_name = group.name
# Get servers where this group's inbounds are deployed
deployed_servers = await sync_to_async(
lambda: list(
ServerInbound.objects.filter(
inbound__in=group.inbounds.all(),
active=True
).select_related('server', 'inbound').values_list(
'server__name', 'inbound__name', 'inbound__protocol'
).distinct()
)
)()
# Group by server
for server_name, inbound_name, protocol in deployed_servers:
if server_name not in servers_data:
servers_data[server_name] = []
servers_data[server_name].append({
'group_name': group_name,
'inbound_name': inbound_name,
'protocol': protocol.upper()
})
# Display servers and their subscriptions
for server_name, server_subscriptions in servers_data.items():
message_text += f"🔒 **{server_name}**\n"
for sub_data in server_subscriptions:
message_text += f"{sub_data['group_name']} ({sub_data['protocol']})\n"
message_text += "\n"
# Add subscription link with localized labels
universal_link_label = get_localized_message(update.message.from_user, 'universal_subscription_link')
portal_label = get_localized_message(update.message.from_user, 'web_portal')
all_subs_note = get_localized_message(update.message.from_user, 'all_subscriptions_note')
message_text += f"{universal_link_label}\n"
message_text += f"`{subscription_link}`\n\n"
message_text += all_subs_note
# Create back navigation keyboard with only back button
from telegram import ReplyKeyboardMarkup, KeyboardButton
back_btn = get_localized_button(update.message.from_user, 'back')
keyboard = [
[KeyboardButton(back_btn)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
sent_message = await update.message.reply_text(
message_text,
reply_markup=reply_markup,
parse_mode='Markdown'
)
else:
# No active subscriptions
no_subs_msg = get_localized_message(update.message.from_user, 'no_subscriptions')
sent_message = await update.message.reply_text(no_subs_msg)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Sent all-in-one subscription to user {user.username}")
except Exception as e:
logger.error(f"Error handling all-in-one selection: {e}")
error_msg = get_localized_message(update.message.from_user, 'error_loading_subscriptions')
await update.message.reply_text(error_msg)
async def _handle_back_to_main(self, update: Update, user):
"""Handle back button - return to main menu"""
try:
# Create main keyboard with localized buttons
2025-08-15 16:33:23 +03:00
reply_markup = await self._create_main_keyboard(update.message.from_user)
2025-08-15 04:02:22 +03:00
help_text = get_localized_message(update.message.from_user, 'help_text')
sent_message = await update.message.reply_text(
help_text,
reply_markup=reply_markup
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Returned user {user.username} to main menu")
except Exception as e:
logger.error(f"Error handling back button: {e}")
await self._send_help_message(update)
async def _show_new_user_keyboard(self, update: Update):
"""Show keyboard with request access button for new users"""
try:
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Get localized button and message
request_access_btn = get_localized_button(update.message.from_user, 'request_access')
welcome_msg = get_localized_message(update.message.from_user, 'new_user_welcome')
# Create keyboard with request access button
keyboard = [
[KeyboardButton(request_access_btn)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
sent_message = await update.message.reply_text(
welcome_msg,
reply_markup=reply_markup
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Showed new user keyboard to {update.message.from_user.username or update.message.from_user.id}")
except Exception as e:
logger.error(f"Error showing new user keyboard: {e}")
async def _show_pending_request_message(self, update: Update, custom_message: str = None):
"""Show pending request message to users with pending access requests"""
try:
# Use custom message or default pending message
if custom_message:
message_text = custom_message
else:
message_text = get_localized_message(update.message.from_user, 'pending_request_msg')
sent_message = await update.message.reply_text(message_text)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Showed pending request message to {update.message.from_user.username or update.message.from_user.id}")
except Exception as e:
logger.error(f"Error showing pending request message: {e}")
async def _link_telegram_to_user(self, user, telegram_user):
"""Link Telegram account to existing VPN user"""
try:
user.telegram_user_id = telegram_user.id
user.telegram_username = telegram_user.username or ""
user.telegram_first_name = telegram_user.first_name or ""
user.telegram_last_name = telegram_user.last_name or ""
await sync_to_async(user.save)()
logger.info(f"Linked Telegram user @{telegram_user.username} (ID: {telegram_user.id}) to existing VPN user {user.username}")
except Exception as e:
logger.error(f"Error linking Telegram to user: {e}")
raise
def _sync_status_on_startup(self):
"""No database status to sync - only lock file matters"""
pass
def auto_start_if_enabled(self):
"""Auto-start bot if enabled in settings"""
try:
bot_settings = BotSettings.get_settings()
if bot_settings.enabled and bot_settings.bot_token and not self._running:
logger.info("Auto-starting bot (enabled in settings)")
self.start()
return True
else:
if not bot_settings.enabled:
logger.info("Bot auto-start skipped: disabled in settings")
elif not bot_settings.bot_token:
logger.info("Bot auto-start skipped: no token configured")
elif self._running:
logger.info("Bot auto-start skipped: already running")
except Exception as e:
# Don't log as error if it's just a lock conflict
if "could not acquire lock" in str(e).lower():
logger.info(f"Bot auto-start skipped: {e}")
else:
logger.error(f"Failed to auto-start bot: {e}")
return False
2025-08-15 04:53:30 +03:00
async def _handle_guide_command(self, update: Update):
"""Handle guide button - show platform selection"""
try:
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Get localized buttons
android_btn = get_localized_button(update.message.from_user, 'android')
ios_btn = get_localized_button(update.message.from_user, 'ios')
web_portal_btn = get_localized_button(update.message.from_user, 'web_portal')
back_btn = get_localized_button(update.message.from_user, 'back')
# Create platform selection keyboard
keyboard = [
[KeyboardButton(android_btn), KeyboardButton(ios_btn)],
[KeyboardButton(web_portal_btn)],
[KeyboardButton(back_btn)]
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
# Get localized messages
guide_title = get_localized_message(update.message.from_user, 'guide_title')
choose_platform = get_localized_message(update.message.from_user, 'guide_choose_platform')
message_text = f"{guide_title}\n\n{choose_platform}"
sent_message = await update.message.reply_text(
message_text,
reply_markup=reply_markup,
parse_mode='Markdown'
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Showed guide platform selection to user")
except Exception as e:
logger.error(f"Error handling guide command: {e}")
async def _handle_android_guide(self, update: Update):
"""Handle Android guide selection"""
try:
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Create back navigation keyboard
back_btn = get_localized_button(update.message.from_user, 'back')
keyboard = [
[KeyboardButton(back_btn)]
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
# Get Android guide text
android_guide = get_localized_message(update.message.from_user, 'android_guide')
sent_message = await update.message.reply_text(
android_guide,
reply_markup=reply_markup,
parse_mode='Markdown',
disable_web_page_preview=True
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Sent Android guide to user")
except Exception as e:
logger.error(f"Error handling Android guide: {e}")
async def _handle_ios_guide(self, update: Update):
"""Handle iOS guide selection"""
try:
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Create back navigation keyboard
back_btn = get_localized_button(update.message.from_user, 'back')
keyboard = [
[KeyboardButton(back_btn)]
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
# Get iOS guide text
ios_guide = get_localized_message(update.message.from_user, 'ios_guide')
sent_message = await update.message.reply_text(
ios_guide,
reply_markup=reply_markup,
parse_mode='Markdown',
disable_web_page_preview=True
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Sent iOS guide to user")
except Exception as e:
logger.error(f"Error handling iOS guide: {e}")
async def _handle_web_portal(self, update: Update, user):
"""Handle web portal button - send portal link"""
try:
from django.conf import settings
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Get the base URL for portal links from settings
base_url = getattr(settings, 'EXTERNAL_ADDRESS', 'https://your-server.com')
# Parse base_url to ensure it has https:// scheme
if not base_url.startswith(('http://', 'https://')):
base_url = f"https://{base_url}"
# Generate user portal link
portal_link = f"{base_url}/u/{user.hash}"
# Create back navigation keyboard
back_btn = get_localized_button(update.message.from_user, 'back')
keyboard = [
[KeyboardButton(back_btn)]
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
# Get localized messages
portal_label = get_localized_message(update.message.from_user, 'web_portal')
portal_description = get_localized_message(update.message.from_user, 'web_portal_description')
message_text = f"{portal_label}\n{portal_link}\n\n{portal_description}"
sent_message = await update.message.reply_text(
message_text,
reply_markup=reply_markup,
parse_mode='Markdown',
disable_web_page_preview=True
)
# Save outgoing message
await self._save_outgoing_message(
sent_message,
update.message.from_user
)
logger.info(f"Sent web portal link to user {user.username}")
except Exception as e:
logger.error(f"Error handling web portal: {e}")
2025-08-15 04:02:22 +03:00
@property
def is_running(self):
"""Check if bot is running"""
return self._running and self._bot_thread and self._bot_thread.is_alive()