This commit is contained in:
Corrado Mulas
2026-06-19 16:02:10 +02:00
parent fea971f6fd
commit 2f09978de6
10 changed files with 355 additions and 137 deletions

View File

@@ -6,14 +6,19 @@ from src.config import (
)
from src.services.user_service import UserService
from src.models.user import User
from src.utils.qr_backup import decode_qr_image_payload, normalize_qr_payload
logger = logging.getLogger(__name__)
class AdminHandler:
"""Handler class for admin configuration commands"""
def __init__(self, user_service: UserService):
def __init__(self, user_service: UserService, telegram_client=None):
self.user_service = user_service
self.telegram_client = telegram_client
def set_client(self, telegram_client):
self.telegram_client = telegram_client
def _ensure_user_record(self, user_id: int) -> tuple:
user = self.user_service.get_user_by_id(user_id)
@@ -30,6 +35,66 @@ class AdminHandler:
created_note = " (placeholder created)" if created else ""
username = f"@{user.username}" if user.username else user.name
return f"{username} (ID: {user_id}){created_note}"
def _is_numeric_id(self, value) -> bool:
return str(value).strip().lstrip("-").isdigit()
def _user_from_entity(self, entity, fallback_username: str = None) -> User:
username = getattr(entity, "username", None) or fallback_username
return User(
id=entity.id,
username=username.lstrip("@") if username else None,
first_name=getattr(entity, "first_name", None) or getattr(entity, "title", None),
last_name=getattr(entity, "last_name", None),
)
async def _resolve_user_identifier(self, identifier) -> tuple:
identifier = str(identifier).strip().rstrip(",")
if not identifier:
raise ValueError("Empty user identifier")
if self._is_numeric_id(identifier):
user_id = int(identifier)
if self.telegram_client:
try:
entity = await self.telegram_client.get_entity(user_id)
user = self._user_from_entity(entity)
existed = self.user_service.get_user_by_id(user.id) is not None
if not self.user_service.save_user(user):
raise RuntimeError(f"Failed to save user {user.id}")
return user.id, user, not existed
except Exception:
logger.info("Could not resolve numeric user ID %s, creating placeholder", user_id)
user, created = self._ensure_user_record(user_id)
return user_id, user, created
if not self.telegram_client:
raise RuntimeError("Telegram client is required to resolve usernames")
username = identifier.lstrip("@")
entity = await self.telegram_client.get_entity(username)
user = self._user_from_entity(entity, fallback_username=username)
existed = self.user_service.get_user_by_id(user.id) is not None
if not self.user_service.save_user(user):
raise RuntimeError(f"Failed to save user {user.id}")
return user.id, user, not existed
async def _resolve_user_identifiers(self, identifiers: List) -> tuple:
resolved = []
created_users = []
errors = []
for identifier in identifiers:
try:
user_id, _, created = await self._resolve_user_identifier(identifier)
resolved.append(user_id)
if created:
created_users.append(user_id)
except Exception as e:
logger.error(f"Failed to resolve user identifier {identifier}: {e}")
errors.append(f"{identifier}: {e}")
return resolved, created_users, errors
def verify_access(self, chat_id: int) -> tuple:
"""Verify admin access"""
@@ -44,13 +109,13 @@ class AdminHandler:
try:
default_users = get_default_group_users()
if not default_users:
return "📋 No default users configured yet.\n\nUse `/admin_set_users <user_id1> <user_id2> ...` to configure them."
return "📋 No default users configured yet.\n\nUse `/admin_set_users <id_or_username1> <id_or_username2> ...` to configure them."
user_list = []
for user_id in default_users:
user = self.user_service.get_user_by_id(user_id)
if user:
user_list.append(f"{user.username} (ID: {user_id})")
user_list.append(self._format_user(user, user_id))
else:
user_list.append(f" • Unknown User (ID: {user_id})")
@@ -59,7 +124,7 @@ class AdminHandler:
logger.error(f"Error getting default users: {e}")
return f"❌ Error retrieving default users: {str(e)}"
async def handle_set_default_users(self, chat_id: int, user_ids: List[int]) -> str:
async def handle_set_default_users(self, chat_id: int, user_ids: List) -> str:
"""Set default users for new groups (Admin only)"""
is_admin, error = self.verify_access(chat_id)
if not is_admin:
@@ -67,16 +132,11 @@ class AdminHandler:
try:
if not user_ids:
return "❌ Please provide at least one user ID.\n\nUsage: `/admin_set_users <user_id1> <user_id2> ...`"
return "❌ Please provide at least one user ID or username.\n\nUsage: `/admin_set_users <id_or_username1> <id_or_username2> ...`"
valid_user_ids = []
created_users = []
for user_id in user_ids:
_, created = self._ensure_user_record(user_id)
valid_user_ids.append(user_id)
if created:
created_users.append(user_id)
valid_user_ids, created_users, errors = await self._resolve_user_identifiers(user_ids)
if not valid_user_ids:
return "❌ No valid users could be resolved.\n" + "\n".join(errors)
# Set default users
if set_default_group_users(valid_user_ids):
@@ -88,7 +148,9 @@ class AdminHandler:
response = "✅ Default users updated successfully:\n" + "\n".join(user_list)
if created_users:
response += "\n\n Placeholder records were created for new Telegram IDs."
response += "\n\n New user records were created."
if errors:
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
return response
else:
@@ -97,7 +159,7 @@ class AdminHandler:
logger.error(f"Error setting default users: {e}")
return f"❌ Error setting default users: {str(e)}"
async def handle_add_to_default_users(self, chat_id: int, user_ids: List[int]) -> str:
async def handle_add_to_default_users(self, chat_id: int, user_ids: List) -> str:
"""Add users to existing default users list (Admin only)"""
is_admin, error = self.verify_access(chat_id)
if not is_admin:
@@ -105,22 +167,18 @@ class AdminHandler:
try:
if not user_ids:
return "❌ Please provide at least one user ID.\n\nUsage: `/admin_add_users <user_id1> <user_id2> ...`"
return "❌ Please provide at least one user ID or username.\n\nUsage: `/admin_add_users <id_or_username1> <id_or_username2> ...`"
current_users = get_default_group_users()
valid_user_ids = []
created_users = []
for user_id in user_ids:
if user_id not in current_users:
_, created = self._ensure_user_record(user_id)
valid_user_ids.append(user_id)
if created:
created_users.append(user_id)
resolved_user_ids, created_users, errors = await self._resolve_user_identifiers(user_ids)
valid_user_ids = [user_id for user_id in resolved_user_ids if user_id not in current_users]
if not valid_user_ids:
return " All provided users are already in the default list."
response = " All resolved users are already in the default list."
if errors:
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
return response
# Add to default users
updated_users = current_users + valid_user_ids
@@ -133,7 +191,9 @@ class AdminHandler:
response = "✅ Users added to default list:\n" + "\n".join(user_list)
if created_users:
response += "\n\n Placeholder records were created for new Telegram IDs."
response += "\n\n New user records were created."
if errors:
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
return response
else:
@@ -142,7 +202,7 @@ class AdminHandler:
logger.error(f"Error adding to default users: {e}")
return f"❌ Error adding to default users: {str(e)}"
async def handle_remove_from_default_users(self, chat_id: int, user_ids: List[int]) -> str:
async def handle_remove_from_default_users(self, chat_id: int, user_ids: List) -> str:
"""Remove users from default users list (Admin only)"""
is_admin, error = self.verify_access(chat_id)
if not is_admin:
@@ -150,16 +210,20 @@ class AdminHandler:
try:
if not user_ids:
return "❌ Please provide at least one user ID.\n\nUsage: `/admin_remove_users <user_id1> <user_id2> ...`"
return "❌ Please provide at least one user ID or username.\n\nUsage: `/admin_remove_users <id_or_username1> <id_or_username2> ...`"
current_users = get_default_group_users()
resolved_user_ids, _, errors = await self._resolve_user_identifiers(user_ids)
# Find users to remove
users_to_remove = [uid for uid in user_ids if uid in current_users]
users_to_remove = [uid for uid in resolved_user_ids if uid in current_users]
if not users_to_remove:
not_in_list = [uid for uid in user_ids if uid not in current_users]
return f" These users are not in the default list: {not_in_list}"
not_in_list = [uid for uid in resolved_user_ids if uid not in current_users]
response = f" These users are not in the default list: {not_in_list}"
if errors:
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
return response
# Remove users
updated_users = [uid for uid in current_users if uid not in users_to_remove]
@@ -172,7 +236,10 @@ class AdminHandler:
else:
user_list.append(f" • Unknown User (ID: {user_id})")
return "✅ Users removed from default list:\n" + "\n".join(user_list)
response = "✅ Users removed from default list:\n" + "\n".join(user_list)
if errors:
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
return response
else:
return "❌ Failed to update default users"
except Exception as e:
@@ -189,18 +256,9 @@ class AdminHandler:
if not username or len(username.strip()) == 0:
return "❌ Please provide a username.\n\nUsage: `/admin_add_user <username>`"
username = username.strip()
# Generate user_id as a hash of the username
import hashlib
user_id = int(hashlib.md5(username.encode()).hexdigest()[:8], 16)
user = User(id=user_id, username=username, first_name=username)
success = self.user_service.save_user(user)
if success:
return f"✅ User {username} added successfully (ID: {user_id})"
else:
return "❌ Failed to add user"
user_id, user, created = await self._resolve_user_identifier(username)
action = "added" if created else "updated"
return f"✅ User @{user.username or username.lstrip('@')} {action} successfully (ID: {user_id})"
except Exception as e:
logger.error(f"Error adding user {username}: {e}")
return f"❌ Error adding user: {str(e)}"
@@ -216,11 +274,20 @@ class AdminHandler:
if qr_data:
return f"📊 Current QR Backup Data:\n`{qr_data}`"
else:
return "📋 No QR backup data configured yet.\n\nUse `/admin_set_qr <qr_code>` to configure it."
return "📋 No QR backup data configured yet.\n\nUse `/admin_set_qr <qr_payload>` to configure it."
except Exception as e:
logger.error(f"Error getting QR backup: {e}")
return f"❌ Error retrieving QR backup: {str(e)}"
def _store_qr_backup_payload(self, qr_data: str) -> tuple:
payload = normalize_qr_payload(qr_data)
if not payload:
return False, "❌ QR backup data cannot be empty.\n\nUsage: `/admin_set_qr <qr_payload>`", None
if set_qr_backup_data(payload):
return True, f"✅ QR backup data updated successfully!\n\nData: `{payload}`", payload
return False, "❌ Failed to save QR backup data", None
async def handle_set_qr_backup(self, chat_id: int, qr_data: str) -> str:
"""Set QR backup data (Admin only)"""
is_admin, error = self.verify_access(chat_id)
@@ -228,16 +295,31 @@ class AdminHandler:
return error
try:
if not qr_data or len(qr_data.strip()) == 0:
return "❌ QR backup data cannot be empty.\n\nUsage: `/admin_set_qr <qr_code>`"
if set_qr_backup_data(qr_data):
return f"✅ QR backup data updated successfully!\n\nData: `{qr_data}`"
else:
return "❌ Failed to save QR backup data"
_, message, _ = self._store_qr_backup_payload(qr_data)
return message
except Exception as e:
logger.error(f"Error setting QR backup: {e}")
return f"❌ Error setting QR backup: {str(e)}"
async def handle_set_qr_backup_from_image(self, chat_id: int, image_bytes: bytes) -> str:
"""Decode and store QR backup data from a forwarded GroupHelp QR image."""
is_admin, error = self.verify_access(chat_id)
if not is_admin:
return error
try:
payload = decode_qr_image_payload(image_bytes)
saved, message, _ = self._store_qr_backup_payload(payload)
if saved:
return message.replace(
"QR backup data updated successfully",
"QR backup image decoded and stored successfully",
1,
)
return message
except Exception as e:
logger.error(f"Error decoding QR backup image: {e}")
return f"❌ Error decoding QR backup image: {str(e)}"
async def handle_admin_help(self, chat_id: int) -> str:
"""Show admin command help (Admin only)"""
@@ -248,18 +330,19 @@ class AdminHandler:
return """🔐 Admin Configuration Commands:
**Default Group Users Management:**
• `/admin_set_users <id1> <id2> ...` - Replace entire default users list
• `/admin_add_users <id1> <id2> ...` - Add users to default list
• `/admin_remove_users <id1> <id2> ...` - Remove users from default list
• `/admin_set_users <id_or_username> ...` - Replace entire default users list
• `/admin_add_users <id_or_username> ...` - Add users to default list
• `/admin_remove_users <id_or_username> ...` - Remove users from default list
• `/admin_get_users` - Show current default users
• `/admin_add_user <username>` - Add new user to database
• `/admin_add_user <username>` - Resolve and save a user in the database
**QR Code Backup:**
• `/admin_get_qr` - Get current QR backup data
• `/admin_set_qr <qr_code>` - Set GroupHelp backup QR data
• `/admin_set_qr <qr_payload>` - Set GroupHelp backup payload rendered as a QR image
• `/admin_set_qr` - Wait for a forwarded `.importbackup` QR image and decode it
**Notes:**
✓ All admin commands work ONLY from the admin chat
✓ Users will be automatically added to new groups
✓ QR backup data is used with GroupHelp `.importbackup`
✓ QR backup data is sent as a QR image with GroupHelp `.importbackup`
✓ All config changes are stored in MongoDB"""