mirror of
https://github.com/house-of-vanity/OutFleet.git
synced 2025-08-21 14:37:16 +00:00
Added tg bot autoconfirm
This commit is contained in:
@@ -8,11 +8,37 @@ from django import forms
|
|||||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
from .models import BotSettings, TelegramMessage, AccessRequest
|
from .models import BotSettings, TelegramMessage, AccessRequest
|
||||||
from .localization import MessageLocalizer
|
from .localization import MessageLocalizer
|
||||||
|
from vpn.models import User
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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):
|
class AccessRequestAdminForm(forms.ModelForm):
|
||||||
"""Custom form for AccessRequest with existing user selection"""
|
"""Custom form for AccessRequest with existing user selection"""
|
||||||
|
|
||||||
@@ -20,10 +46,6 @@ class AccessRequestAdminForm(forms.ModelForm):
|
|||||||
model = AccessRequest
|
model = AccessRequest
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
widgets = {
|
widgets = {
|
||||||
'selected_inbounds': FilteredSelectMultiple(
|
|
||||||
verbose_name='Inbound Templates',
|
|
||||||
is_stacked=False
|
|
||||||
),
|
|
||||||
'selected_subscription_groups': FilteredSelectMultiple(
|
'selected_subscription_groups': FilteredSelectMultiple(
|
||||||
verbose_name='Subscription Groups',
|
verbose_name='Subscription Groups',
|
||||||
is_stacked=False
|
is_stacked=False
|
||||||
@@ -43,13 +65,7 @@ class AccessRequestAdminForm(forms.ModelForm):
|
|||||||
telegram_user_id__isnull=True
|
telegram_user_id__isnull=True
|
||||||
).order_by('username')
|
).order_by('username')
|
||||||
|
|
||||||
# Configure inbound and subscription group fields
|
# Configure 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'
|
|
||||||
|
|
||||||
if 'selected_subscription_groups' in self.fields:
|
if 'selected_subscription_groups' in self.fields:
|
||||||
from vpn.models_xray import SubscriptionGroup
|
from vpn.models_xray import SubscriptionGroup
|
||||||
self.fields['selected_subscription_groups'].queryset = SubscriptionGroup.objects.filter(
|
self.fields['selected_subscription_groups'].queryset = SubscriptionGroup.objects.filter(
|
||||||
@@ -61,12 +77,17 @@ class AccessRequestAdminForm(forms.ModelForm):
|
|||||||
|
|
||||||
@admin.register(BotSettings)
|
@admin.register(BotSettings)
|
||||||
class BotSettingsAdmin(admin.ModelAdmin):
|
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 = (
|
fieldsets = (
|
||||||
('Bot Configuration', {
|
('Bot Configuration', {
|
||||||
'fields': ('bot_token', 'enabled', 'bot_status_display'),
|
'fields': ('bot_token', 'enabled', 'bot_status_display'),
|
||||||
'description': 'Configure bot settings and view current status'
|
'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', {
|
('Connection Settings', {
|
||||||
'fields': ('api_base_url', 'connection_timeout', 'use_proxy', 'proxy_url'),
|
'fields': ('api_base_url', 'connection_timeout', 'use_proxy', 'proxy_url'),
|
||||||
'classes': ('collapse',)
|
'classes': ('collapse',)
|
||||||
@@ -76,7 +97,8 @@ class BotSettingsAdmin(admin.ModelAdmin):
|
|||||||
'classes': ('collapse',)
|
'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):
|
def bot_token_display(self, obj):
|
||||||
"""Mask bot token for security"""
|
"""Mask bot token for security"""
|
||||||
@@ -88,6 +110,60 @@ class BotSettingsAdmin(admin.ModelAdmin):
|
|||||||
return "No token set"
|
return "No token set"
|
||||||
bot_token_display.short_description = "Bot Token"
|
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):
|
def bot_status_display(self, obj):
|
||||||
"""Display bot status with control buttons"""
|
"""Display bot status with control buttons"""
|
||||||
from .bot import TelegramBotManager
|
from .bot import TelegramBotManager
|
||||||
@@ -365,10 +441,9 @@ class AccessRequestAdmin(admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
('VPN Access Configuration', {
|
('VPN Access Configuration', {
|
||||||
'fields': (
|
'fields': (
|
||||||
'selected_inbounds',
|
|
||||||
'selected_subscription_groups',
|
'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', {
|
('Telegram User', {
|
||||||
'fields': (
|
'fields': (
|
||||||
@@ -620,11 +695,12 @@ class AccessRequestAdmin(admin.ModelAdmin):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
def _assign_vpn_access(self, user, access_request):
|
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:
|
try:
|
||||||
from vpn.models_xray import UserSubscription, SubscriptionGroup
|
from vpn.models_xray import UserSubscription
|
||||||
|
|
||||||
# Assign subscription groups
|
# Assign subscription groups
|
||||||
|
group_count = 0
|
||||||
for subscription_group in access_request.selected_subscription_groups.all():
|
for subscription_group in access_request.selected_subscription_groups.all():
|
||||||
user_subscription, created = UserSubscription.objects.get_or_create(
|
user_subscription, created = UserSubscription.objects.get_or_create(
|
||||||
user=user,
|
user=user,
|
||||||
@@ -633,48 +709,16 @@ class AccessRequestAdmin(admin.ModelAdmin):
|
|||||||
)
|
)
|
||||||
if created:
|
if created:
|
||||||
logger.info(f"Assigned subscription group '{subscription_group.name}' to user {user.username}")
|
logger.info(f"Assigned subscription group '{subscription_group.name}' to user {user.username}")
|
||||||
|
group_count += 1
|
||||||
else:
|
else:
|
||||||
# Ensure it's active if it already existed
|
# Ensure it's active if it already existed
|
||||||
if not user_subscription.active:
|
if not user_subscription.active:
|
||||||
user_subscription.active = True
|
user_subscription.active = True
|
||||||
user_subscription.save()
|
user_subscription.save()
|
||||||
logger.info(f"Re-activated subscription group '{subscription_group.name}' for user {user.username}")
|
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
|
logger.info(f"Successfully assigned {group_count} subscription groups to user {user.username}")
|
||||||
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}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error assigning VPN access to user {user.username}: {e}")
|
logger.error(f"Error assigning VPN access to user {user.username}: {e}")
|
||||||
|
@@ -6,7 +6,7 @@ import os
|
|||||||
import fcntl
|
import fcntl
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from telegram import Update, Bot
|
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.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from asgiref.sync import sync_to_async
|
from asgiref.sync import sync_to_async
|
||||||
@@ -125,6 +125,619 @@ class TelegramBotManager:
|
|||||||
self._release_lock()
|
self._release_lock()
|
||||||
raise e
|
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):
|
def stop(self):
|
||||||
"""Stop the bot"""
|
"""Stop the bot"""
|
||||||
if not self._running:
|
if not self._running:
|
||||||
@@ -217,6 +830,7 @@ class TelegramBotManager:
|
|||||||
|
|
||||||
# Add handlers
|
# Add handlers
|
||||||
self._application.add_handler(CommandHandler("start", self._handle_start))
|
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.TEXT & ~filters.COMMAND, self._handle_message))
|
||||||
self._application.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, self._handle_other))
|
self._application.add_handler(MessageHandler(filters.ALL & ~filters.COMMAND, self._handle_other))
|
||||||
|
|
||||||
@@ -276,19 +890,7 @@ class TelegramBotManager:
|
|||||||
|
|
||||||
if user_response['action'] == 'existing_user':
|
if user_response['action'] == 'existing_user':
|
||||||
# User already exists - show keyboard with options
|
# User already exists - show keyboard with options
|
||||||
from telegram import ReplyKeyboardMarkup, KeyboardButton
|
reply_markup = await self._create_main_keyboard(update.message.from_user)
|
||||||
|
|
||||||
# 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
|
|
||||||
)
|
|
||||||
|
|
||||||
help_text = get_localized_message(update.message.from_user, 'help_text')
|
help_text = get_localized_message(update.message.from_user, 'help_text')
|
||||||
sent_message = await update.message.reply_text(
|
sent_message = await update.message.reply_text(
|
||||||
@@ -331,6 +933,7 @@ class TelegramBotManager:
|
|||||||
# Get localized button texts for comparison
|
# Get localized button texts for comparison
|
||||||
access_btn = get_localized_button(update.message.from_user, 'access')
|
access_btn = get_localized_button(update.message.from_user, 'access')
|
||||||
guide_btn = get_localized_button(update.message.from_user, 'guide')
|
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')
|
all_in_one_btn = get_localized_button(update.message.from_user, 'all_in_one')
|
||||||
back_btn = get_localized_button(update.message.from_user, 'back')
|
back_btn = get_localized_button(update.message.from_user, 'back')
|
||||||
group_prefix = get_localized_button(update.message.from_user, 'group_prefix')
|
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'])
|
await self._handle_access_command(update, user_response['user'])
|
||||||
elif update.message.text == guide_btn:
|
elif update.message.text == guide_btn:
|
||||||
await self._handle_guide_command(update)
|
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):
|
elif update.message.text.startswith(group_prefix):
|
||||||
# Handle specific group selection
|
# Handle specific group selection
|
||||||
group_name = update.message.text.replace(group_prefix, "")
|
group_name = update.message.text.replace(group_prefix, "")
|
||||||
@@ -653,16 +1262,7 @@ class TelegramBotManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# No active subscriptions - show main keyboard
|
# No active subscriptions - show main keyboard
|
||||||
access_button = get_localized_button(update.message.from_user, 'access')
|
reply_markup = await self._create_main_keyboard(update.message.from_user)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
no_subs_msg = get_localized_message(update.message.from_user, 'no_subscriptions')
|
no_subs_msg = get_localized_message(update.message.from_user, 'no_subscriptions')
|
||||||
sent_message = await update.message.reply_text(
|
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}")
|
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
|
return request
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -721,17 +1325,7 @@ class TelegramBotManager:
|
|||||||
"""Send help message for unrecognized commands"""
|
"""Send help message for unrecognized commands"""
|
||||||
try:
|
try:
|
||||||
# Create main keyboard for existing users with localized buttons
|
# Create main keyboard for existing users with localized buttons
|
||||||
from telegram import ReplyKeyboardMarkup, KeyboardButton
|
reply_markup = await self._create_main_keyboard(update.message.from_user)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
help_text = get_localized_message(update.message.from_user, 'help_text')
|
help_text = get_localized_message(update.message.from_user, 'help_text')
|
||||||
sent_message = await update.message.reply_text(
|
sent_message = await update.message.reply_text(
|
||||||
@@ -978,19 +1572,8 @@ class TelegramBotManager:
|
|||||||
async def _handle_back_to_main(self, update: Update, user):
|
async def _handle_back_to_main(self, update: Update, user):
|
||||||
"""Handle back button - return to main menu"""
|
"""Handle back button - return to main menu"""
|
||||||
try:
|
try:
|
||||||
from telegram import ReplyKeyboardMarkup, KeyboardButton
|
|
||||||
|
|
||||||
# Create main keyboard with localized buttons
|
# Create main keyboard with localized buttons
|
||||||
access_btn = get_localized_button(update.message.from_user, 'access')
|
reply_markup = await self._create_main_keyboard(update.message.from_user)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
help_text = get_localized_message(update.message.from_user, 'help_text')
|
help_text = get_localized_message(update.message.from_user, 'help_text')
|
||||||
sent_message = await update.message.reply_text(
|
sent_message = await update.message.reply_text(
|
||||||
|
@@ -41,6 +41,18 @@ MESSAGES = {
|
|||||||
'guide_choose_platform': "Select your device platform:",
|
'guide_choose_platform': "Select your device platform:",
|
||||||
'web_portal_description': "_Web portal shows your access list on one convenient page with some statistics._",
|
'web_portal_description': "_Web portal shows your access list on one convenient page with some statistics._",
|
||||||
'servers_in_group': "🔒 **Servers in group:**",
|
'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.",
|
'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.",
|
'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': {
|
'buttons': {
|
||||||
@@ -52,7 +64,15 @@ MESSAGES = {
|
|||||||
'all_in_one': "🌍 All-in-one",
|
'all_in_one': "🌍 All-in-one",
|
||||||
'back': "⬅️ Back",
|
'back': "⬅️ Back",
|
||||||
'group_prefix': "Group: ",
|
'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': {
|
'ru': {
|
||||||
@@ -88,6 +108,18 @@ MESSAGES = {
|
|||||||
'guide_choose_platform': "Выберите платформу вашего устройства:",
|
'guide_choose_platform': "Выберите платформу вашего устройства:",
|
||||||
'web_portal_description': "_Веб-портал показывает список ваших доступов на одной удобной странице с некоторой статистикой._",
|
'web_portal_description': "_Веб-портал показывает список ваших доступов на одной удобной странице с некоторой статистикой._",
|
||||||
'servers_in_group': "🔒 **Серверы в группе:**",
|
'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Нажмите иконку обновления рядом со списком серверов для обновления подписки.",
|
'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Нажмите иконку обновления рядом со списком серверов для обновления подписки.",
|
'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': {
|
'buttons': {
|
||||||
@@ -99,7 +131,15 @@ MESSAGES = {
|
|||||||
'all_in_one': "🌍 Все в одном",
|
'all_in_one': "🌍 Все в одном",
|
||||||
'back': "⬅️ Назад",
|
'back': "⬅️ Назад",
|
||||||
'group_prefix': "Группа: ",
|
'group_prefix': "Группа: ",
|
||||||
'request_access': "🔑 Запросить доступ"
|
'request_access': "🔑 Запросить доступ",
|
||||||
|
# Admin buttons
|
||||||
|
'access_requests': "📋 Запросы на доступ",
|
||||||
|
'approve': "✅ Одобрить",
|
||||||
|
'reject': "❌ Отклонить",
|
||||||
|
'details': "👁 Подробности",
|
||||||
|
'confirm_approval': "✅ Подтвердить одобрение",
|
||||||
|
'confirm_rejection': "❌ Подтвердить отклонение",
|
||||||
|
'cancel': "🚫 Отмена"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
telegram_bot/migrations/0010_botsettings_telegram_admins.py
Normal file
20
telegram_bot/migrations/0010_botsettings_telegram_admins.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
@@ -33,6 +33,12 @@ class BotSettings(models.Model):
|
|||||||
default=30,
|
default=30,
|
||||||
help_text="Connection timeout in seconds"
|
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)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
10
vpn/admin.py
10
vpn/admin.py
@@ -157,12 +157,10 @@ except ImportError:
|
|||||||
|
|
||||||
# Add subscription management to User admin
|
# Add subscription management to User admin
|
||||||
try:
|
try:
|
||||||
from vpn.admin.user import add_subscription_management_to_user
|
from vpn.admin_xray import add_subscription_management_to_user
|
||||||
from django.contrib.admin import site
|
from vpn.admin.user import UserAdmin
|
||||||
for model, admin_instance in site._registry.items():
|
add_subscription_management_to_user(UserAdmin)
|
||||||
if model.__name__ == 'User' and hasattr(admin_instance, 'fieldsets'):
|
logger.info("✅ Successfully added subscription management to User admin")
|
||||||
add_subscription_management_to_user(admin_instance.__class__)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to add subscription management: {e}")
|
logger.error(f"Failed to add subscription management: {e}")
|
||||||
|
|
||||||
|
@@ -23,8 +23,8 @@ class UserAdmin(BaseVPNAdmin):
|
|||||||
form = UserForm
|
form = UserForm
|
||||||
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
|
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
|
||||||
search_fields = ('username', 'hash', 'telegram_user_id', 'telegram_username')
|
search_fields = ('username', 'hash', 'telegram_user_id', 'telegram_username')
|
||||||
readonly_fields = ('hash_link', 'vpn_access_summary', 'user_statistics_summary', 'telegram_info_display')
|
readonly_fields = ('hash_link', 'vpn_access_summary', 'user_statistics_summary', 'telegram_info_display', 'subscription_management_info')
|
||||||
inlines = [] # All VPN access info is now in vpn_access_summary
|
inlines = [] # Inlines will be added by subscription management function
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('User Information', {
|
('User Information', {
|
||||||
@@ -42,6 +42,11 @@ class UserAdmin(BaseVPNAdmin):
|
|||||||
'fields': ('user_statistics_summary',),
|
'fields': ('user_statistics_summary',),
|
||||||
'classes': ('wide',)
|
'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')
|
@admin.display(description='VPN Access Summary')
|
||||||
@@ -450,6 +455,99 @@ class UserAdmin(BaseVPNAdmin):
|
|||||||
html += '</div>'
|
html += '</div>'
|
||||||
return mark_safe(html)
|
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')
|
@admin.display(description='Allowed servers', ordering='server_count')
|
||||||
def server_count(self, obj):
|
def server_count(self, obj):
|
||||||
return obj.server_count
|
return obj.server_count
|
||||||
|
@@ -614,14 +614,15 @@ class UserSubscriptionInline(admin.TabularInline):
|
|||||||
|
|
||||||
|
|
||||||
# Extension for User admin
|
# 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 subscription management to existing User admin"""
|
||||||
|
|
||||||
# Add inline only - no fieldset or widget
|
# Add inline to the User admin class
|
||||||
if hasattr(UserAdmin, 'inlines'):
|
if hasattr(UserAdminClass, 'inlines'):
|
||||||
UserAdmin.inlines = list(UserAdmin.inlines) + [UserSubscriptionInline]
|
if UserSubscriptionInline not in UserAdminClass.inlines:
|
||||||
|
UserAdminClass.inlines = list(UserAdminClass.inlines) + [UserSubscriptionInline]
|
||||||
else:
|
else:
|
||||||
UserAdmin.inlines = [UserSubscriptionInline]
|
UserAdminClass.inlines = [UserSubscriptionInline]
|
||||||
|
|
||||||
|
|
||||||
# UserSubscription admin will be integrated into unified Subscriptions admin
|
# UserSubscription admin will be integrated into unified Subscriptions admin
|
||||||
|
Reference in New Issue
Block a user