Added tg bot autoconfirm

This commit is contained in:
Ultradesu
2025-08-15 16:33:23 +03:00
parent 57cef79748
commit 95e0d08b51
8 changed files with 904 additions and 114 deletions

View File

@@ -8,11 +8,37 @@ from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from .models import BotSettings, TelegramMessage, AccessRequest
from .localization import MessageLocalizer
from vpn.models import User
import logging
logger = logging.getLogger(__name__)
class BotSettingsAdminForm(forms.ModelForm):
"""Custom form for BotSettings with Telegram admin selection"""
class Meta:
model = BotSettings
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Show all users for telegram_admins selection
if 'telegram_admins' in self.fields:
self.fields['telegram_admins'].queryset = User.objects.all().order_by('username')
self.fields['telegram_admins'].help_text = (
"Select users who will have admin access in the bot. "
"Users will get admin rights when they connect to the bot with their Telegram account."
)
def clean_telegram_admins(self):
"""Validate that selected admins have telegram_user_id or telegram_username"""
admins = self.cleaned_data.get('telegram_admins')
# No validation needed - admins can be selected even without telegram connection
# They will get admin rights when they connect via bot
return admins
class AccessRequestAdminForm(forms.ModelForm):
"""Custom form for AccessRequest with existing user selection"""
@@ -20,10 +46,6 @@ class AccessRequestAdminForm(forms.ModelForm):
model = AccessRequest
fields = '__all__'
widgets = {
'selected_inbounds': FilteredSelectMultiple(
verbose_name='Inbound Templates',
is_stacked=False
),
'selected_subscription_groups': FilteredSelectMultiple(
verbose_name='Subscription Groups',
is_stacked=False
@@ -43,13 +65,7 @@ class AccessRequestAdminForm(forms.ModelForm):
telegram_user_id__isnull=True
).order_by('username')
# Configure inbound and subscription group fields
if 'selected_inbounds' in self.fields:
from vpn.models_xray import Inbound
self.fields['selected_inbounds'].queryset = Inbound.objects.all().order_by('name')
self.fields['selected_inbounds'].label = 'Inbound Templates'
self.fields['selected_inbounds'].help_text = 'Select inbound templates to assign to this user'
# Configure subscription group fields
if 'selected_subscription_groups' in self.fields:
from vpn.models_xray import SubscriptionGroup
self.fields['selected_subscription_groups'].queryset = SubscriptionGroup.objects.filter(
@@ -61,12 +77,17 @@ class AccessRequestAdminForm(forms.ModelForm):
@admin.register(BotSettings)
class BotSettingsAdmin(admin.ModelAdmin):
list_display = ('__str__', 'enabled', 'bot_token_display', 'updated_at')
form = BotSettingsAdminForm
list_display = ('__str__', 'enabled', 'bot_token_display', 'admin_count_display', 'updated_at')
fieldsets = (
('Bot Configuration', {
'fields': ('bot_token', 'enabled', 'bot_status_display'),
'description': 'Configure bot settings and view current status'
}),
('Admin Management', {
'fields': ('telegram_admins', 'admin_info_display'),
'description': 'Select users with linked Telegram accounts who will have admin access in the bot'
}),
('Connection Settings', {
'fields': ('api_base_url', 'connection_timeout', 'use_proxy', 'proxy_url'),
'classes': ('collapse',)
@@ -76,7 +97,8 @@ class BotSettingsAdmin(admin.ModelAdmin):
'classes': ('collapse',)
}),
)
readonly_fields = ('created_at', 'updated_at', 'bot_status_display')
readonly_fields = ('created_at', 'updated_at', 'bot_status_display', 'admin_info_display')
filter_horizontal = ('telegram_admins',)
def bot_token_display(self, obj):
"""Mask bot token for security"""
@@ -88,6 +110,60 @@ class BotSettingsAdmin(admin.ModelAdmin):
return "No token set"
bot_token_display.short_description = "Bot Token"
def admin_count_display(self, obj):
"""Display count of Telegram admins"""
count = obj.telegram_admins.count()
if count == 0:
return "No admins"
elif count == 1:
return "1 admin"
else:
return f"{count} admins"
admin_count_display.short_description = "Telegram Admins"
def admin_info_display(self, obj):
"""Display detailed admin information"""
if not obj.pk:
return "Save settings first to manage admins"
admins = obj.telegram_admins.all()
if not admins.exists():
html = '<div style="background: #fff3cd; padding: 10px; border-radius: 4px; border-left: 4px solid #ffc107;">'
html += '<p style="margin: 0; color: #856404;"><strong>⚠️ No Telegram admins configured</strong></p>'
html += '<p style="margin: 5px 0 0 0; color: #856404;">Select users above to give them admin access in the Telegram bot.</p>'
html += '</div>'
else:
html = '<div style="background: #d4edda; padding: 10px; border-radius: 4px; border-left: 4px solid #28a745;">'
html += f'<p style="margin: 0; color: #155724;"><strong>✅ {admins.count()} Telegram admin(s) configured</strong></p>'
html += '<div style="margin-top: 8px;">'
for admin in admins:
html += '<div style="background: white; margin: 4px 0; padding: 6px 10px; border-radius: 3px; border: 1px solid #c3e6cb;">'
html += f'<strong>{admin.username}</strong>'
if admin.telegram_username:
html += f' (@{admin.telegram_username})'
html += f' <small style="color: #6c757d;">ID: {admin.telegram_user_id}</small>'
if admin.first_name or admin.last_name:
name_parts = []
if admin.first_name:
name_parts.append(admin.first_name)
if admin.last_name:
name_parts.append(admin.last_name)
html += f'<br><small style="color: #6c757d;">Name: {" ".join(name_parts)}</small>'
html += '</div>'
html += '</div>'
html += '<p style="margin: 8px 0 0 0; color: #155724; font-size: 12px;">These users will receive notifications about new access requests and can approve/reject them directly in Telegram.</p>'
html += '</div>'
return format_html(html)
admin_info_display.short_description = "Admin Configuration"
def bot_status_display(self, obj):
"""Display bot status with control buttons"""
from .bot import TelegramBotManager
@@ -365,10 +441,9 @@ class AccessRequestAdmin(admin.ModelAdmin):
}),
('VPN Access Configuration', {
'fields': (
'selected_inbounds',
'selected_subscription_groups',
),
'description': 'Select inbound templates and subscription groups to assign to the user'
'description': 'Select subscription groups to assign to the user'
}),
('Telegram User', {
'fields': (
@@ -620,11 +695,12 @@ class AccessRequestAdmin(admin.ModelAdmin):
raise
def _assign_vpn_access(self, user, access_request):
"""Assign selected inbounds and subscription groups to the user"""
"""Assign selected subscription groups to the user"""
try:
from vpn.models_xray import UserSubscription, SubscriptionGroup
from vpn.models_xray import UserSubscription
# Assign subscription groups
group_count = 0
for subscription_group in access_request.selected_subscription_groups.all():
user_subscription, created = UserSubscription.objects.get_or_create(
user=user,
@@ -633,48 +709,16 @@ class AccessRequestAdmin(admin.ModelAdmin):
)
if created:
logger.info(f"Assigned subscription group '{subscription_group.name}' to user {user.username}")
group_count += 1
else:
# Ensure it's active if it already existed
if not user_subscription.active:
user_subscription.active = True
user_subscription.save()
logger.info(f"Re-activated subscription group '{subscription_group.name}' for user {user.username}")
group_count += 1
# Handle individual inbounds - create a custom subscription group for them
selected_inbounds = access_request.selected_inbounds.all()
if selected_inbounds.exists():
# Create a custom subscription group for this user's individual inbounds
custom_group_name = f"Custom_{user.username}_{access_request.id}"
custom_group, created = SubscriptionGroup.objects.get_or_create(
name=custom_group_name,
defaults={
'description': f'Custom inbounds for {user.username} from Telegram request',
'is_active': True
}
)
if created:
# Add selected inbounds to the custom group
custom_group.inbounds.set(selected_inbounds)
logger.info(f"Created custom subscription group '{custom_group_name}' with {selected_inbounds.count()} inbounds")
else:
# Update existing custom group
custom_group.inbounds.add(*selected_inbounds)
logger.info(f"Updated custom subscription group '{custom_group_name}' with additional inbounds")
# Assign the custom group to the user
user_subscription, created = UserSubscription.objects.get_or_create(
user=user,
subscription_group=custom_group,
defaults={'active': True}
)
if created:
logger.info(f"Assigned custom subscription group to user {user.username}")
inbound_count = selected_inbounds.count()
group_count = access_request.selected_subscription_groups.count()
logger.info(f"Successfully assigned {group_count} subscription groups and {inbound_count} individual inbounds to user {user.username}")
logger.info(f"Successfully assigned {group_count} subscription groups to user {user.username}")
except Exception as e:
logger.error(f"Error assigning VPN access to user {user.username}: {e}")

View File

@@ -6,7 +6,7 @@ import os
import fcntl
from typing import Optional
from telegram import Update, Bot
from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes
from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, ContextTypes
from django.utils import timezone
from django.conf import settings
from asgiref.sync import sync_to_async
@@ -125,6 +125,619 @@ class TelegramBotManager:
self._release_lock()
raise e
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)
def stop(self):
"""Stop the bot"""
if not self._running:
@@ -217,6 +830,7 @@ class TelegramBotManager:
# Add handlers
self._application.add_handler(CommandHandler("start", self._handle_start))
self._application.add_handler(CallbackQueryHandler(self._handle_callback_query))
self._application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, self._handle_message))
self._application.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, self._handle_other))
@@ -276,19 +890,7 @@ class TelegramBotManager:
if user_response['action'] == 'existing_user':
# User already exists - show keyboard with options
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Create keyboard for registered users with localized buttons
access_button = get_localized_button(update.message.from_user, 'access')
guide_button = get_localized_button(update.message.from_user, 'guide')
keyboard = [
[KeyboardButton(access_button), KeyboardButton(guide_button)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
reply_markup = await self._create_main_keyboard(update.message.from_user)
help_text = get_localized_message(update.message.from_user, 'help_text')
sent_message = await update.message.reply_text(
@@ -331,6 +933,7 @@ class TelegramBotManager:
# Get localized button texts for comparison
access_btn = get_localized_button(update.message.from_user, 'access')
guide_btn = get_localized_button(update.message.from_user, 'guide')
access_requests_btn = get_localized_button(update.message.from_user, 'access_requests')
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')
@@ -340,6 +943,12 @@ class TelegramBotManager:
await self._handle_access_command(update, user_response['user'])
elif update.message.text == guide_btn:
await self._handle_guide_command(update)
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)
elif update.message.text.startswith(group_prefix):
# Handle specific group selection
group_name = update.message.text.replace(group_prefix, "")
@@ -653,16 +1262,7 @@ class TelegramBotManager:
)
else:
# No active subscriptions - show main keyboard
access_button = get_localized_button(update.message.from_user, 'access')
guide_button = get_localized_button(update.message.from_user, 'guide')
keyboard = [
[KeyboardButton(access_button), KeyboardButton(guide_button)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
reply_markup = await self._create_main_keyboard(update.message.from_user)
no_subs_msg = get_localized_message(update.message.from_user, 'no_subscriptions')
sent_message = await update.message.reply_text(
@@ -711,6 +1311,10 @@ class TelegramBotManager:
)
logger.info(f"Created access request for user {telegram_user.username or telegram_user.id}")
# Notify admins about new request
await self._notify_admins_new_request(request)
return request
except Exception as e:
@@ -721,17 +1325,7 @@ class TelegramBotManager:
"""Send help message for unrecognized commands"""
try:
# Create main keyboard for existing users with localized buttons
from telegram import ReplyKeyboardMarkup, KeyboardButton
access_button = get_localized_button(update.message.from_user, 'access')
guide_button = get_localized_button(update.message.from_user, 'guide')
keyboard = [
[KeyboardButton(access_button), KeyboardButton(guide_button)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
reply_markup = await self._create_main_keyboard(update.message.from_user)
help_text = get_localized_message(update.message.from_user, 'help_text')
sent_message = await update.message.reply_text(
@@ -978,19 +1572,8 @@ class TelegramBotManager:
async def _handle_back_to_main(self, update: Update, user):
"""Handle back button - return to main menu"""
try:
from telegram import ReplyKeyboardMarkup, KeyboardButton
# Create main keyboard with localized buttons
access_btn = get_localized_button(update.message.from_user, 'access')
guide_btn = get_localized_button(update.message.from_user, 'guide')
keyboard = [
[KeyboardButton(access_btn), KeyboardButton(guide_btn)],
]
reply_markup = ReplyKeyboardMarkup(
keyboard,
resize_keyboard=True,
one_time_keyboard=False
)
reply_markup = await self._create_main_keyboard(update.message.from_user)
help_text = get_localized_message(update.message.from_user, 'help_text')
sent_message = await update.message.reply_text(

View File

@@ -41,6 +41,18 @@ MESSAGES = {
'guide_choose_platform': "Select your device platform:",
'web_portal_description': "_Web portal shows your access list on one convenient page with some statistics._",
'servers_in_group': "🔒 **Servers in group:**",
# Admin messages
'admin_new_request_notification': "🔔 **New Access Request**\n\n👤 **User:** {user_info}\n📱 **Telegram:** {telegram_info}\n📅 **Date:** {date}\n\n💬 **Message:** {message}",
'admin_access_requests_title': "📋 **Pending Access Requests**",
'admin_no_pending_requests': "✅ No pending access requests",
'admin_request_item': "👤 **{user_info}**\n📅 {date}\n💬 _{message_preview}_",
'admin_choose_subscription_groups': "📦 **Choose Subscription Groups for {user_info}:**\n\nSelect groups to assign to this user:",
'admin_approval_success': "✅ **Request Approved!**\n\n👤 User: {user_info}\n📦 Groups: {groups}\n\nUser has been notified and given access.",
'admin_rejection_success': "❌ **Request Rejected**\n\n👤 User: {user_info}\n\nUser has been notified.",
'admin_request_already_processed': "⚠️ This request has already been processed by another admin.",
'admin_error_processing': "❌ Error processing request: {error}",
'android_guide': "🤖 **Android Setup Guide**\n\n**Step 1: Install the app**\nDownload V2RayTUN from Google Play:\nhttps://play.google.com/store/apps/details?id=com.v2raytun.android\n\n**Step 2: Add subscription**\n• Open the app\n• Tap the **+** button in the top right corner\n• Paste your subscription link from the bot\n• The app will automatically load all VPN servers\n\n**Step 3: Connect**\n• Choose a server from the list\n• Tap **Connect**\n• All your traffic will now go through VPN\n\n**💡 Useful settings:**\n• In settings, enable direct access for banking apps and local sites\n• You can choose specific apps to use VPN while others use direct connection\n\n**🔄 If VPN stops working:**\nTap the refresh icon next to the server list to update your subscription.",
'ios_guide': " **iOS Setup Guide**\n\n**Step 1: Install the app**\nDownload V2RayTUN from App Store:\nhttps://apps.apple.com/us/app/v2raytun/id6476628951\n\n**Step 2: Add subscription**\n• Open the app\n• Tap the **+** button in the top right corner\n• Paste your subscription link from the bot\n• The app will automatically load all VPN servers\n\n**Step 3: Connect**\n• Choose a server from the list\n• Tap **Connect**\n• All your traffic will now go through VPN\n\n**⚠️ Note for iOS users:**\nCurrently, only VLESS protocol works reliably on iOS. Other protocols may have connectivity issues.\n\n**💡 Useful settings:**\n• In settings, enable direct access for banking apps and local sites to improve performance\n\n**🔄 If VPN stops working:**\nTap the refresh icon next to the server list to update your subscription.",
'buttons': {
@@ -52,7 +64,15 @@ MESSAGES = {
'all_in_one': "🌍 All-in-one",
'back': "⬅️ Back",
'group_prefix': "Group: ",
'request_access': "🔑 Request Access"
'request_access': "🔑 Request Access",
# Admin buttons
'access_requests': "📋 Access Requests",
'approve': "✅ Approve",
'reject': "❌ Reject",
'details': "👁 Details",
'confirm_approval': "✅ Confirm Approval",
'confirm_rejection': "❌ Confirm Rejection",
'cancel': "🚫 Cancel"
}
},
'ru': {
@@ -88,6 +108,18 @@ MESSAGES = {
'guide_choose_platform': "Выберите платформу вашего устройства:",
'web_portal_description': "_Веб-портал показывает список ваших доступов на одной удобной странице с некоторой статистикой._",
'servers_in_group': "🔒 **Серверы в группе:**",
# Admin messages
'admin_new_request_notification': "🔔 **Новый запрос на доступ**\n\n👤 **Пользователь:** {user_info}\n📱 **Telegram:** {telegram_info}\n📅 **Дата:** {date}\n\n💬 **Сообщение:** {message}",
'admin_access_requests_title': "📋 **Ожидающие запросы на доступ**",
'admin_no_pending_requests': "✅ Нет ожидающих запросов на доступ",
'admin_request_item': "👤 **{user_info}**\n📅 {date}\n💬 _{message_preview}_",
'admin_choose_subscription_groups': "📦 **Выберите группы подписки для {user_info}:**\n\nВыберите группы для назначения этому пользователю:",
'admin_approval_success': "✅ **Запрос одобрен!**\n\n👤 Пользователь: {user_info}\n📦 Группы: {groups}\n\nПользователь уведомлен и получил доступ.",
'admin_rejection_success': "❌ **Запрос отклонен**\n\n👤 Пользователь: {user_info}\n\nПользователь уведомлен.",
'admin_request_already_processed': "⚠️ Этот запрос уже обработан другим администратором.",
'admin_error_processing': "❌ Ошибка обработки запроса: {error}",
'android_guide': "🤖 **Руководство для Android**\n\n**Шаг 1: Установите приложение**\nСкачайте V2RayTUN из Google Play:\nhttps://play.google.com/store/apps/details?id=com.v2raytun.android\n\n**Шаг 2: Добавьте подписку**\n• Откройте приложение\n• Нажмите кнопку **+** в правом верхнем углу\n• Вставьте ссылку на подписку из бота\n• Приложение автоматически загрузит список VPN серверов\n\n**Шаг 3: Подключитесь**\n• Выберите сервер из списка\n• Нажмите **Подключиться**\n• Весь ваш трафик будет проходить через VPN\n\n**💡 Полезные настройки:**\nВ настройках включите прямой доступ для банковских приложений и местных сайтов\n• Вы можете выбрать конкретные приложения для использования VPN, в то время как остальные будут работать напрямую\n\n**🔄 Если VPN перестал работать:**\nНажмите иконку обновления рядом со списком серверов для обновления подписки.",
'ios_guide': " **Руководство для iOS**\n\n**Шаг 1: Установите приложение**\nСкачайте V2RayTUN из App Store:\nhttps://apps.apple.com/us/app/v2raytun/id6476628951\n\n**Шаг 2: Добавьте подписку**\n• Откройте приложение\n• Нажмите кнопку **+** в правом верхнем углу\n• Вставьте ссылку на подписку из бота\n• Приложение автоматически загрузит список VPN серверов\n\n**Шаг 3: Подключитесь**\n• Выберите сервер из списка\n• Нажмите **Подключиться**\n• Весь ваш трафик будет проходить через VPN\n\n**⚠️ Важно для пользователей iOS:**\nВ настоящее время на iOS стабильно работает только протокол VLESS. Другие протоколы могут иметь проблемы с подключением.\n\n**💡 Полезные настройки:**\nВ настройках включите прямой доступ для банковских приложений и местных сайтов для улучшения производительности\n\n**🔄 Если VPN перестал работать:**\nНажмите иконку обновления рядом со списком серверов для обновления подписки.",
'buttons': {
@@ -99,7 +131,15 @@ MESSAGES = {
'all_in_one': "🌍 Все в одном",
'back': "⬅️ Назад",
'group_prefix': "Группа: ",
'request_access': "🔑 Запросить доступ"
'request_access': "🔑 Запросить доступ",
# Admin buttons
'access_requests': "📋 Запросы на доступ",
'approve': "✅ Одобрить",
'reject': "❌ Отклонить",
'details': "👁 Подробности",
'confirm_approval': "✅ Подтвердить одобрение",
'confirm_rejection': "❌ Подтвердить отклонение",
'cancel': "🚫 Отмена"
}
}
}

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.1.7 on 2025-08-15 13:00
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('telegram_bot', '0009_accessrequest_selected_inbounds_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='botsettings',
name='telegram_admins',
field=models.ManyToManyField(blank=True, help_text='Users with linked Telegram accounts who will have admin access in the bot', related_name='bot_admin_settings', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -33,6 +33,12 @@ class BotSettings(models.Model):
default=30,
help_text="Connection timeout in seconds"
)
telegram_admins = models.ManyToManyField(
User,
blank=True,
related_name='bot_admin_settings',
help_text="Users with linked Telegram accounts who will have admin access in the bot"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

View File

@@ -157,12 +157,10 @@ except ImportError:
# Add subscription management to User admin
try:
from vpn.admin.user import add_subscription_management_to_user
from django.contrib.admin import site
for model, admin_instance in site._registry.items():
if model.__name__ == 'User' and hasattr(admin_instance, 'fieldsets'):
add_subscription_management_to_user(admin_instance.__class__)
break
from vpn.admin_xray import add_subscription_management_to_user
from vpn.admin.user import UserAdmin
add_subscription_management_to_user(UserAdmin)
logger.info("✅ Successfully added subscription management to User admin")
except Exception as e:
logger.error(f"Failed to add subscription management: {e}")

View File

@@ -23,8 +23,8 @@ class UserAdmin(BaseVPNAdmin):
form = UserForm
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
search_fields = ('username', 'hash', 'telegram_user_id', 'telegram_username')
readonly_fields = ('hash_link', 'vpn_access_summary', 'user_statistics_summary', 'telegram_info_display')
inlines = [] # All VPN access info is now in vpn_access_summary
readonly_fields = ('hash_link', 'vpn_access_summary', 'user_statistics_summary', 'telegram_info_display', 'subscription_management_info')
inlines = [] # Inlines will be added by subscription management function
fieldsets = (
('User Information', {
@@ -42,6 +42,11 @@ class UserAdmin(BaseVPNAdmin):
'fields': ('user_statistics_summary',),
'classes': ('wide',)
}),
('Subscription Management', {
'fields': ('subscription_management_info',),
'classes': ('wide',),
'description': 'Manage user\'s Xray subscription groups. Use the "User\'s Subscription Groups" section below to add/remove subscriptions.'
}),
)
@admin.display(description='VPN Access Summary')
@@ -450,6 +455,99 @@ class UserAdmin(BaseVPNAdmin):
html += '</div>'
return mark_safe(html)
@admin.display(description='Subscription Management')
def subscription_management_info(self, obj):
"""Display subscription management information and quick access"""
if not obj.pk:
return "Save user first to manage subscriptions"
try:
from vpn.models_xray import UserSubscription, SubscriptionGroup
# Get user's current subscriptions
user_subscriptions = UserSubscription.objects.filter(user=obj).select_related('subscription_group')
active_subs = user_subscriptions.filter(active=True)
inactive_subs = user_subscriptions.filter(active=False)
# Get available subscription groups
all_groups = SubscriptionGroup.objects.filter(is_active=True)
subscribed_group_ids = user_subscriptions.values_list('subscription_group_id', flat=True)
available_groups = all_groups.exclude(id__in=subscribed_group_ids)
html = '<div style="background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0;">'
html += '<h4 style="margin: 0 0 15px 0; color: #495057;">🚀 Xray Subscription Management</h4>'
# Active subscriptions
if active_subs.exists():
html += '<div style="margin-bottom: 15px;">'
html += '<h5 style="color: #28a745; margin: 0 0 8px 0;">✅ Active Subscriptions</h5>'
for sub in active_subs:
html += f'<div style="background: #d4edda; padding: 8px 12px; border-radius: 4px; margin: 4px 0; display: flex; justify-content: space-between; align-items: center;">'
html += f'<span><strong>{sub.subscription_group.name}</strong>'
if sub.subscription_group.description:
html += f' - {sub.subscription_group.description[:50]}{"..." if len(sub.subscription_group.description) > 50 else ""}'
html += f'</span>'
html += f'<small style="color: #155724;">Since: {sub.created_at.strftime("%Y-%m-%d")}</small>'
html += f'</div>'
html += '</div>'
# Inactive subscriptions
if inactive_subs.exists():
html += '<div style="margin-bottom: 15px;">'
html += '<h5 style="color: #dc3545; margin: 0 0 8px 0;">❌ Inactive Subscriptions</h5>'
for sub in inactive_subs:
html += f'<div style="background: #f8d7da; padding: 8px 12px; border-radius: 4px; margin: 4px 0;">'
html += f'<span style="color: #721c24;"><strong>{sub.subscription_group.name}</strong></span>'
html += f'</div>'
html += '</div>'
# Available subscription groups
if available_groups.exists():
html += '<div style="margin-bottom: 15px;">'
html += '<h5 style="color: #007cba; margin: 0 0 8px 0;"> Available Subscription Groups</h5>'
html += '<div style="display: flex; gap: 8px; flex-wrap: wrap;">'
for group in available_groups[:10]: # Limit to avoid clutter
html += f'<span style="background: #cce7ff; color: #004085; padding: 4px 8px; border-radius: 3px; font-size: 12px;">'
html += f'{group.name}'
html += f'</span>'
if available_groups.count() > 10:
html += f'<span style="color: #6c757d; font-style: italic;">+{available_groups.count() - 10} more...</span>'
html += '</div>'
html += '</div>'
# Quick access links
html += '<div style="border-top: 1px solid #dee2e6; padding-top: 12px; margin-top: 15px;">'
html += '<h5 style="margin: 0 0 8px 0; color: #495057;">🔗 Quick Access</h5>'
html += '<div style="display: flex; gap: 10px; flex-wrap: wrap;">'
# Link to standalone UserSubscription admin
subscription_admin_url = f"/admin/vpn/usersubscription/?user__id__exact={obj.id}"
html += f'<a href="{subscription_admin_url}" class="button" style="background: #007cba; color: white; text-decoration: none; padding: 6px 12px; border-radius: 3px; font-size: 12px;">📋 Manage All Subscriptions</a>'
# Link to add new subscription
add_subscription_url = f"/admin/vpn/usersubscription/add/?user={obj.id}"
html += f'<a href="{add_subscription_url}" class="button" style="background: #28a745; color: white; text-decoration: none; padding: 6px 12px; border-radius: 3px; font-size: 12px;"> Add New Subscription</a>'
# Link to subscription groups admin
groups_admin_url = "/admin/vpn/subscriptiongroup/"
html += f'<a href="{groups_admin_url}" class="button" style="background: #17a2b8; color: white; text-decoration: none; padding: 6px 12px; border-radius: 3px; font-size: 12px;">⚙️ Manage Groups</a>'
html += '</div>'
html += '</div>'
# Statistics
total_subs = user_subscriptions.count()
if total_subs > 0:
html += '<div style="border-top: 1px solid #dee2e6; padding-top: 8px; margin-top: 10px;">'
html += f'<small style="color: #6c757d;">📊 Total: {total_subs} subscription(s) | Active: {active_subs.count()} | Inactive: {inactive_subs.count()}</small>'
html += '</div>'
html += '</div>'
return mark_safe(html)
except Exception as e:
return mark_safe(f'<div style="background: #f8d7da; padding: 10px; border-radius: 4px; color: #721c24;">❌ Error loading subscription management: {e}</div>')
@admin.display(description='Allowed servers', ordering='server_count')
def server_count(self, obj):
return obj.server_count

View File

@@ -614,14 +614,15 @@ class UserSubscriptionInline(admin.TabularInline):
# Extension for User admin
def add_subscription_management_to_user(UserAdmin):
def add_subscription_management_to_user(UserAdminClass):
"""Add subscription management to existing User admin"""
# Add inline only - no fieldset or widget
if hasattr(UserAdmin, 'inlines'):
UserAdmin.inlines = list(UserAdmin.inlines) + [UserSubscriptionInline]
# Add inline to the User admin class
if hasattr(UserAdminClass, 'inlines'):
if UserSubscriptionInline not in UserAdminClass.inlines:
UserAdminClass.inlines = list(UserAdminClass.inlines) + [UserSubscriptionInline]
else:
UserAdmin.inlines = [UserSubscriptionInline]
UserAdminClass.inlines = [UserSubscriptionInline]
# UserSubscription admin will be integrated into unified Subscriptions admin