Fix path
This commit is contained in:
@@ -20,15 +20,16 @@ The following commands can ONLY be run from the admin chat:
|
|||||||
```
|
```
|
||||||
/admin_add_user <username> - Add new user to database
|
/admin_add_user <username> - Add new user to database
|
||||||
/admin_get_users - View default users list
|
/admin_get_users - View default users list
|
||||||
/admin_set_users <id1> <id2> ... - Replace entire default users list
|
/admin_set_users <id_or_username> ... - Replace entire default users list
|
||||||
/admin_add_users <id1> <id2> ... - Add users to default list
|
/admin_add_users <id_or_username> ... - Add users to default list
|
||||||
/admin_remove_users <id1> <id2> - Remove users from default list
|
/admin_remove_users <id_or_username> - Remove users from default list
|
||||||
```
|
```
|
||||||
|
|
||||||
#### GroupHelp QR Code Backup
|
#### GroupHelp QR Code Backup
|
||||||
```
|
```
|
||||||
/admin_get_qr - Retrieve GroupHelp backup QR data
|
/admin_get_qr - Retrieve GroupHelp backup QR data
|
||||||
/admin_set_qr <qr_code> - Store GroupHelp backup QR data for `.importbackup`
|
/admin_set_qr <qr_payload> - Store GroupHelp backup payload rendered as a QR image for `.importbackup`
|
||||||
|
/admin_set_qr - Wait for a forwarded `.importbackup` QR image and decode it
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Help
|
#### Help
|
||||||
@@ -119,6 +120,11 @@ Bot: ✅ Default users updated successfully:
|
|||||||
|
|
||||||
Admin: /admin_set_qr 0001a8ac0123456789abcdef...
|
Admin: /admin_set_qr 0001a8ac0123456789abcdef...
|
||||||
Bot: ✅ QR backup data updated successfully!
|
Bot: ✅ QR backup data updated successfully!
|
||||||
|
|
||||||
|
Admin: /admin_set_qr
|
||||||
|
Bot: 📷 Forward the original GroupHelp QR image message here.
|
||||||
|
Admin: forwards original `.importbackup` QR image message
|
||||||
|
Bot: ✅ QR backup image decoded and stored successfully!
|
||||||
```
|
```
|
||||||
|
|
||||||
### User Usage (any chat)
|
### User Usage (any chat)
|
||||||
@@ -215,14 +221,14 @@ MONGODB_COLLECTION=ghconfig
|
|||||||
### Test GroupHelp QR Backup
|
### Test GroupHelp QR Backup
|
||||||
```bash
|
```bash
|
||||||
# In admin chat:
|
# In admin chat:
|
||||||
/admin_set_qr myqrcode123
|
/admin_set_qr new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||||
# ✅ QR backup data updated successfully!
|
# ✅ QR backup data updated successfully!
|
||||||
|
|
||||||
/admin_get_qr
|
/admin_get_qr
|
||||||
# 📊 Current QR Backup Data: myqrcode123
|
# 📊 Current QR Backup Data: new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||||
|
|
||||||
# In the target group:
|
# During !newgrp, GroupFactory sends the payload as a QR image with:
|
||||||
.importbackup myqrcode123
|
.importbackup
|
||||||
```
|
```
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|||||||
@@ -131,30 +131,38 @@ This will create ProjectBeta with only alice and bob, regardless of the default
|
|||||||
|
|
||||||
### What is QR Backup Data?
|
### What is QR Backup Data?
|
||||||
|
|
||||||
QR backup data is the GroupHelp settings backup payload generated by GroupHelp itself. It is used to copy GroupHelp settings, such as welcome-message configuration, from one group to another by pasting the QR payload as the body of a `.importbackup` message.
|
QR backup data is the GroupHelp settings backup payload generated by GroupHelp itself. It is used to copy GroupHelp settings, such as welcome-message configuration, from one group to another by rendering the payload as a QR image and sending it with a `.importbackup` caption.
|
||||||
|
|
||||||
### Step 1: Generate or Obtain QR Code
|
### Step 1: Generate or Obtain QR Code
|
||||||
|
|
||||||
Generate the QR backup from GroupHelp in the source group, then store the decoded/pasted payload here so it can be reused when importing those settings into another group.
|
Generate the QR backup from GroupHelp in the source group. You can either store the decoded payload directly, or start the interactive importer and forward the original GroupHelp QR image message.
|
||||||
|
|
||||||
### Step 2: Set GroupHelp QR Backup Data
|
### Step 2: Set GroupHelp QR Backup Data
|
||||||
|
|
||||||
Store the GroupHelp QR backup data in the database:
|
Store the GroupHelp QR backup payload directly:
|
||||||
|
|
||||||
```
|
```
|
||||||
/set_qr_backup YOUR_QR_CODE_HERE
|
/admin_set_qr YOUR_QR_PAYLOAD_HERE
|
||||||
```
|
```
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```
|
```
|
||||||
/set_qr_backup 0001a8ac0123456789abcdef0123456789abcdef01234567
|
/admin_set_qr new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or start the interactive QR importer:
|
||||||
|
|
||||||
|
```
|
||||||
|
/admin_set_qr
|
||||||
|
```
|
||||||
|
|
||||||
|
Then forward the original GroupHelp QR image message into the staff chat. The forwarded image message must have `.importbackup` as its caption/body. GroupFactory decodes the QR image and stores the decoded payload.
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
```
|
```
|
||||||
✅ QR backup data updated successfully!
|
✅ QR backup data updated successfully!
|
||||||
|
|
||||||
Data: `0001a8ac0123456789abcdef0123456789abcdef01234567`
|
Data: `new_390530c569ec8902a6cbd8482d51d70d2857d4a0`
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Retrieve GroupHelp QR Backup Data
|
### Step 3: Retrieve GroupHelp QR Backup Data
|
||||||
@@ -162,21 +170,21 @@ Data: `0001a8ac0123456789abcdef0123456789abcdef01234567`
|
|||||||
Retrieve the stored GroupHelp QR backup data anytime:
|
Retrieve the stored GroupHelp QR backup data anytime:
|
||||||
|
|
||||||
```
|
```
|
||||||
/get_qr_backup
|
/admin_get_qr
|
||||||
```
|
```
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
```
|
```
|
||||||
📊 Current QR Backup Data:
|
📊 Current QR Backup Data:
|
||||||
`0001a8ac0123456789abcdef0123456789abcdef01234567`
|
`new_390530c569ec8902a6cbd8482d51d70d2857d4a0`
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 4: Use QR Backup with GroupHelp
|
### Step 4: Use QR Backup with GroupHelp
|
||||||
|
|
||||||
Once you have the QR backup data stored, you can:
|
Once you have the QR backup data stored, you can:
|
||||||
1. Retrieve it with `/get_qr_backup`
|
1. Retrieve it with `/admin_get_qr`
|
||||||
2. Paste it into the target group as the message body for `.importbackup`
|
2. Let GroupFactory render it as a QR image during `!newgrp`
|
||||||
3. Let GroupHelp import the backed-up settings into that group
|
3. Let GroupHelp import the backed-up settings from the `.importbackup` image message
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -241,10 +249,10 @@ The configuration data is stored in MongoDB with the following structure:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Retrieve the stored GroupHelp QR payload:
|
# Retrieve the stored GroupHelp QR payload:
|
||||||
/get_qr_backup
|
/admin_get_qr
|
||||||
|
|
||||||
# In the target group, paste the payload with:
|
# During !newgrp, GroupFactory sends the payload as a QR image with:
|
||||||
.importbackup <paste_qr_code_here>
|
.importbackup
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -265,8 +273,8 @@ The configuration data is stored in MongoDB with the following structure:
|
|||||||
|
|
||||||
### Issue: GroupHelp QR backup data appears empty
|
### Issue: GroupHelp QR backup data appears empty
|
||||||
**Solution:**
|
**Solution:**
|
||||||
1. Check if data was stored: `/get_qr_backup`
|
1. Check if data was stored: `/admin_get_qr`
|
||||||
2. Re-set the data: `/set_qr_backup <your_qr_code>`
|
2. Re-set the data: `/admin_set_qr <your_qr_payload>` or use `/admin_set_qr` and forward the `.importbackup` QR image
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -309,8 +317,9 @@ FACTORY_BOT_USERNAME=your_bot_username
|
|||||||
- `/set_default_users <id1> <id2> ...` - Set default users (replace)
|
- `/set_default_users <id1> <id2> ...` - Set default users (replace)
|
||||||
- `/add_default_users <id1> <id2> ...` - Add users to default list
|
- `/add_default_users <id1> <id2> ...` - Add users to default list
|
||||||
- `/remove_default_users <id1> <id2> ...` - Remove users from default list
|
- `/remove_default_users <id1> <id2> ...` - Remove users from default list
|
||||||
- `/get_qr_backup` - View GroupHelp QR backup data
|
- `/admin_get_qr` - View GroupHelp QR backup data
|
||||||
- `/set_qr_backup <data>` - Set GroupHelp QR backup data
|
- `/admin_set_qr <qr_payload>` - Set GroupHelp QR backup data directly
|
||||||
|
- `/admin_set_qr` - Decode a forwarded `.importbackup` QR image
|
||||||
- `/config_help` - Show configuration command help
|
- `/config_help` - Show configuration command help
|
||||||
- `/help` - Show all available commands
|
- `/help` - Show all available commands
|
||||||
|
|
||||||
@@ -362,7 +371,7 @@ db.ghconfig.find({key: 'qr_backup_data'})
|
|||||||
## Operational Notes
|
## Operational Notes
|
||||||
|
|
||||||
1. QR backup data is a GroupHelp configuration payload, not a Telegram or Telethon session.
|
1. QR backup data is a GroupHelp configuration payload, not a Telegram or Telethon session.
|
||||||
2. Use `.importbackup` in the target group to import the stored GroupHelp settings.
|
2. During `!newgrp`, GroupFactory sends `.importbackup` with the stored GroupHelp settings as a QR image.
|
||||||
3. Limit configuration commands to trusted chats so group settings are changed intentionally.
|
3. Limit configuration commands to trusted chats so group settings are changed intentionally.
|
||||||
4. Monitor command usage if multiple operators manage the same groups.
|
4. Monitor command usage if multiple operators manage the same groups.
|
||||||
|
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -7,7 +7,7 @@ A Telegram user API service for managing groups with MongoDB backend and admin-o
|
|||||||
- ✅ Create and manage Telegram groups with default user lists
|
- ✅ Create and manage Telegram groups with default user lists
|
||||||
- ✅ Admin-only configuration (requires STAFF_CHAT_ID)
|
- ✅ Admin-only configuration (requires STAFF_CHAT_ID)
|
||||||
- ✅ User management with MongoDB storage
|
- ✅ User management with MongoDB storage
|
||||||
- ✅ GroupHelp settings backup QR storage for `.importbackup`
|
- ✅ GroupHelp settings backup payload rendered as a QR image for `.importbackup`
|
||||||
- ✅ Interactive admin role selection for group creators
|
- ✅ Interactive admin role selection for group creators
|
||||||
- ✅ Modular architecture with separation of concerns
|
- ✅ Modular architecture with separation of concerns
|
||||||
- ✅ Telegram user API integration using Telethon
|
- ✅ Telegram user API integration using Telethon
|
||||||
@@ -88,11 +88,12 @@ docker run telegram-groupfactory
|
|||||||
### Admin Commands (Admin Chat Only)
|
### Admin Commands (Admin Chat Only)
|
||||||
```
|
```
|
||||||
/admin_add_user <username> - Add user to database
|
/admin_add_user <username> - Add user to database
|
||||||
/admin_set_users <id1> <id2> ... - Set default users for groups
|
/admin_set_users <id_or_username> ... - Set default users for groups
|
||||||
/admin_add_users <id1> <id2> ... - Add users to default list
|
/admin_add_users <id_or_username> ... - Add users to default list
|
||||||
/admin_remove_users <id1> <id2> - Remove users from default list
|
/admin_remove_users <id_or_username> - Remove users from default list
|
||||||
/admin_get_users - Show current default users
|
/admin_get_users - Show current default users
|
||||||
/admin_set_qr <qr_code> - Store GroupHelp backup QR data
|
/admin_set_qr <qr_payload> - Store GroupHelp backup payload rendered as a QR image
|
||||||
|
/admin_set_qr - Decode a forwarded `.importbackup` QR image
|
||||||
/admin_get_qr - Retrieve GroupHelp backup QR data
|
/admin_get_qr - Retrieve GroupHelp backup QR data
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,7 @@ pymongo>=4.0.0
|
|||||||
python-dotenv>=0.19.0
|
python-dotenv>=0.19.0
|
||||||
fastapi>=0.95.0
|
fastapi>=0.95.0
|
||||||
uvicorn>=0.21.0
|
uvicorn>=0.21.0
|
||||||
pydantic>=1.10.0
|
pydantic>=1.10.0
|
||||||
|
numpy>=1.26.0
|
||||||
|
opencv-python-headless>=4.9.0.80
|
||||||
|
qrcode[pil]>=7.4.2
|
||||||
|
|||||||
@@ -6,14 +6,19 @@ from src.config import (
|
|||||||
)
|
)
|
||||||
from src.services.user_service import UserService
|
from src.services.user_service import UserService
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
|
from src.utils.qr_backup import decode_qr_image_payload, normalize_qr_payload
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class AdminHandler:
|
class AdminHandler:
|
||||||
"""Handler class for admin configuration commands"""
|
"""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.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:
|
def _ensure_user_record(self, user_id: int) -> tuple:
|
||||||
user = self.user_service.get_user_by_id(user_id)
|
user = self.user_service.get_user_by_id(user_id)
|
||||||
@@ -30,6 +35,66 @@ class AdminHandler:
|
|||||||
created_note = " (placeholder created)" if created else ""
|
created_note = " (placeholder created)" if created else ""
|
||||||
username = f"@{user.username}" if user.username else user.name
|
username = f"@{user.username}" if user.username else user.name
|
||||||
return f" • {username} (ID: {user_id}){created_note}"
|
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:
|
def verify_access(self, chat_id: int) -> tuple:
|
||||||
"""Verify admin access"""
|
"""Verify admin access"""
|
||||||
@@ -44,13 +109,13 @@ class AdminHandler:
|
|||||||
try:
|
try:
|
||||||
default_users = get_default_group_users()
|
default_users = get_default_group_users()
|
||||||
if not default_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 = []
|
user_list = []
|
||||||
for user_id in default_users:
|
for user_id in default_users:
|
||||||
user = self.user_service.get_user_by_id(user_id)
|
user = self.user_service.get_user_by_id(user_id)
|
||||||
if user:
|
if user:
|
||||||
user_list.append(f" • {user.username} (ID: {user_id})")
|
user_list.append(self._format_user(user, user_id))
|
||||||
else:
|
else:
|
||||||
user_list.append(f" • Unknown User (ID: {user_id})")
|
user_list.append(f" • Unknown User (ID: {user_id})")
|
||||||
|
|
||||||
@@ -59,7 +124,7 @@ class AdminHandler:
|
|||||||
logger.error(f"Error getting default users: {e}")
|
logger.error(f"Error getting default users: {e}")
|
||||||
return f"❌ Error retrieving default users: {str(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)"""
|
"""Set default users for new groups (Admin only)"""
|
||||||
is_admin, error = self.verify_access(chat_id)
|
is_admin, error = self.verify_access(chat_id)
|
||||||
if not is_admin:
|
if not is_admin:
|
||||||
@@ -67,16 +132,11 @@ class AdminHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not user_ids:
|
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 = []
|
valid_user_ids, created_users, errors = await self._resolve_user_identifiers(user_ids)
|
||||||
created_users = []
|
if not valid_user_ids:
|
||||||
|
return "❌ No valid users could be resolved.\n" + "\n".join(errors)
|
||||||
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)
|
|
||||||
|
|
||||||
# Set default users
|
# Set default users
|
||||||
if set_default_group_users(valid_user_ids):
|
if set_default_group_users(valid_user_ids):
|
||||||
@@ -88,7 +148,9 @@ class AdminHandler:
|
|||||||
|
|
||||||
response = "✅ Default users updated successfully:\n" + "\n".join(user_list)
|
response = "✅ Default users updated successfully:\n" + "\n".join(user_list)
|
||||||
if created_users:
|
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
|
return response
|
||||||
else:
|
else:
|
||||||
@@ -97,7 +159,7 @@ class AdminHandler:
|
|||||||
logger.error(f"Error setting default users: {e}")
|
logger.error(f"Error setting default users: {e}")
|
||||||
return f"❌ Error setting default users: {str(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)"""
|
"""Add users to existing default users list (Admin only)"""
|
||||||
is_admin, error = self.verify_access(chat_id)
|
is_admin, error = self.verify_access(chat_id)
|
||||||
if not is_admin:
|
if not is_admin:
|
||||||
@@ -105,22 +167,18 @@ class AdminHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not user_ids:
|
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()
|
current_users = get_default_group_users()
|
||||||
|
|
||||||
valid_user_ids = []
|
resolved_user_ids, created_users, errors = await self._resolve_user_identifiers(user_ids)
|
||||||
created_users = []
|
valid_user_ids = [user_id for user_id in resolved_user_ids if user_id not in current_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)
|
|
||||||
|
|
||||||
if not valid_user_ids:
|
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
|
# Add to default users
|
||||||
updated_users = current_users + valid_user_ids
|
updated_users = current_users + valid_user_ids
|
||||||
@@ -133,7 +191,9 @@ class AdminHandler:
|
|||||||
|
|
||||||
response = "✅ Users added to default list:\n" + "\n".join(user_list)
|
response = "✅ Users added to default list:\n" + "\n".join(user_list)
|
||||||
if created_users:
|
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
|
return response
|
||||||
else:
|
else:
|
||||||
@@ -142,7 +202,7 @@ class AdminHandler:
|
|||||||
logger.error(f"Error adding to default users: {e}")
|
logger.error(f"Error adding to default users: {e}")
|
||||||
return f"❌ Error adding to default users: {str(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)"""
|
"""Remove users from default users list (Admin only)"""
|
||||||
is_admin, error = self.verify_access(chat_id)
|
is_admin, error = self.verify_access(chat_id)
|
||||||
if not is_admin:
|
if not is_admin:
|
||||||
@@ -150,16 +210,20 @@ class AdminHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not user_ids:
|
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()
|
current_users = get_default_group_users()
|
||||||
|
resolved_user_ids, _, errors = await self._resolve_user_identifiers(user_ids)
|
||||||
|
|
||||||
# Find users to remove
|
# 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:
|
if not users_to_remove:
|
||||||
not_in_list = [uid for uid in user_ids if uid not in current_users]
|
not_in_list = [uid for uid in resolved_user_ids if uid not in current_users]
|
||||||
return f"ℹ️ These users are not in the default list: {not_in_list}"
|
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
|
# Remove users
|
||||||
updated_users = [uid for uid in current_users if uid not in users_to_remove]
|
updated_users = [uid for uid in current_users if uid not in users_to_remove]
|
||||||
@@ -172,7 +236,10 @@ class AdminHandler:
|
|||||||
else:
|
else:
|
||||||
user_list.append(f" • Unknown User (ID: {user_id})")
|
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:
|
else:
|
||||||
return "❌ Failed to update default users"
|
return "❌ Failed to update default users"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -189,18 +256,9 @@ class AdminHandler:
|
|||||||
if not username or len(username.strip()) == 0:
|
if not username or len(username.strip()) == 0:
|
||||||
return "❌ Please provide a username.\n\nUsage: `/admin_add_user <username>`"
|
return "❌ Please provide a username.\n\nUsage: `/admin_add_user <username>`"
|
||||||
|
|
||||||
username = username.strip()
|
user_id, user, created = await self._resolve_user_identifier(username)
|
||||||
|
action = "added" if created else "updated"
|
||||||
# Generate user_id as a hash of the username
|
return f"✅ User @{user.username or username.lstrip('@')} {action} successfully (ID: {user_id})"
|
||||||
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"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding user {username}: {e}")
|
logger.error(f"Error adding user {username}: {e}")
|
||||||
return f"❌ Error adding user: {str(e)}"
|
return f"❌ Error adding user: {str(e)}"
|
||||||
@@ -216,11 +274,20 @@ class AdminHandler:
|
|||||||
if qr_data:
|
if qr_data:
|
||||||
return f"📊 Current QR Backup Data:\n`{qr_data}`"
|
return f"📊 Current QR Backup Data:\n`{qr_data}`"
|
||||||
else:
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error getting QR backup: {e}")
|
logger.error(f"Error getting QR backup: {e}")
|
||||||
return f"❌ Error retrieving QR backup: {str(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:
|
async def handle_set_qr_backup(self, chat_id: int, qr_data: str) -> str:
|
||||||
"""Set QR backup data (Admin only)"""
|
"""Set QR backup data (Admin only)"""
|
||||||
is_admin, error = self.verify_access(chat_id)
|
is_admin, error = self.verify_access(chat_id)
|
||||||
@@ -228,16 +295,31 @@ class AdminHandler:
|
|||||||
return error
|
return error
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not qr_data or len(qr_data.strip()) == 0:
|
_, message, _ = self._store_qr_backup_payload(qr_data)
|
||||||
return "❌ QR backup data cannot be empty.\n\nUsage: `/admin_set_qr <qr_code>`"
|
return message
|
||||||
|
|
||||||
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"
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting QR backup: {e}")
|
logger.error(f"Error setting QR backup: {e}")
|
||||||
return f"❌ Error setting QR backup: {str(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:
|
async def handle_admin_help(self, chat_id: int) -> str:
|
||||||
"""Show admin command help (Admin only)"""
|
"""Show admin command help (Admin only)"""
|
||||||
@@ -248,18 +330,19 @@ class AdminHandler:
|
|||||||
return """🔐 Admin Configuration Commands:
|
return """🔐 Admin Configuration Commands:
|
||||||
|
|
||||||
**Default Group Users Management:**
|
**Default Group Users Management:**
|
||||||
• `/admin_set_users <id1> <id2> ...` - Replace entire default users list
|
• `/admin_set_users <id_or_username> ...` - Replace entire default users list
|
||||||
• `/admin_add_users <id1> <id2> ...` - Add users to default list
|
• `/admin_add_users <id_or_username> ...` - Add users to default list
|
||||||
• `/admin_remove_users <id1> <id2> ...` - Remove users from default list
|
• `/admin_remove_users <id_or_username> ...` - Remove users from default list
|
||||||
• `/admin_get_users` - Show current default users
|
• `/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:**
|
**QR Code Backup:**
|
||||||
• `/admin_get_qr` - Get current QR backup data
|
• `/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:**
|
**Notes:**
|
||||||
✓ All admin commands work ONLY from the admin chat
|
✓ All admin commands work ONLY from the admin chat
|
||||||
✓ Users will be automatically added to new groups
|
✓ 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"""
|
✓ All config changes are stored in MongoDB"""
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class ConfigHandler:
|
|||||||
if qr_data:
|
if qr_data:
|
||||||
return f"📊 Current QR Backup Data:\n`{qr_data}`"
|
return f"📊 Current QR Backup Data:\n`{qr_data}`"
|
||||||
else:
|
else:
|
||||||
return "📋 No QR backup data configured yet.\n\nUse `/set_qr_backup <qr_code>` to configure it."
|
return "📋 No QR backup data configured yet.\n\nUse `/set_qr_backup <qr_payload>` to configure it."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting QR backup: {e}")
|
logger.error(f"Error getting QR backup: {e}")
|
||||||
return f"❌ Error retrieving QR backup: {str(e)}"
|
return f"❌ Error retrieving QR backup: {str(e)}"
|
||||||
@@ -172,7 +172,7 @@ class ConfigHandler:
|
|||||||
"""Set QR backup data"""
|
"""Set QR backup data"""
|
||||||
try:
|
try:
|
||||||
if not qr_data or len(qr_data.strip()) == 0:
|
if not qr_data or len(qr_data.strip()) == 0:
|
||||||
return "❌ QR backup data cannot be empty.\n\nUsage: `/set_qr_backup <qr_code>`"
|
return "❌ QR backup data cannot be empty.\n\nUsage: `/set_qr_backup <qr_payload>`"
|
||||||
|
|
||||||
if set_qr_backup_data(qr_data):
|
if set_qr_backup_data(qr_data):
|
||||||
return f"✅ QR backup data updated successfully!\n\nData: `{qr_data}`"
|
return f"✅ QR backup data updated successfully!\n\nData: `{qr_data}`"
|
||||||
@@ -194,6 +194,6 @@ class ConfigHandler:
|
|||||||
|
|
||||||
**QR Code Backup:**
|
**QR Code Backup:**
|
||||||
• `/get_qr_backup` - Get current QR backup data
|
• `/get_qr_backup` - Get current QR backup data
|
||||||
• `/set_qr_backup <qr_code>` - Set GroupHelp backup QR data for `.importbackup`
|
• `/set_qr_backup <qr_payload>` - Set GroupHelp backup payload rendered as a QR image for `.importbackup`
|
||||||
|
|
||||||
When a new group is created without specifying users, all configured default users will be automatically added."""
|
When a new group is created without specifying users, all configured default users will be automatically added."""
|
||||||
|
|||||||
102
src/main.py
102
src/main.py
@@ -50,7 +50,9 @@ async def main():
|
|||||||
config['telegram']['api_hash']
|
config['telegram']['api_hash']
|
||||||
)
|
)
|
||||||
group_service.set_client(client)
|
group_service.set_client(client)
|
||||||
|
admin_handler.set_client(client)
|
||||||
group_conversations = {}
|
group_conversations = {}
|
||||||
|
qr_import_sessions = {}
|
||||||
|
|
||||||
async def create_group_from_staff_flow(event, name: str, description: str):
|
async def create_group_from_staff_flow(event, name: str, description: str):
|
||||||
response = await group_handler.handle_create_group(
|
response = await group_handler.handle_create_group(
|
||||||
@@ -65,12 +67,37 @@ async def main():
|
|||||||
|
|
||||||
def is_staff_chat(chat_id: int) -> bool:
|
def is_staff_chat(chat_id: int) -> bool:
|
||||||
return chat_id == staff_chat_id
|
return chat_id == staff_chat_id
|
||||||
|
|
||||||
|
def is_importbackup_body(raw_text: str) -> bool:
|
||||||
|
return (raw_text or "").strip().lower() in (".importbackup", "/importbackup")
|
||||||
|
|
||||||
|
def has_image_media(message) -> bool:
|
||||||
|
if getattr(message, "photo", None):
|
||||||
|
return True
|
||||||
|
document = getattr(message, "document", None)
|
||||||
|
mime_type = getattr(document, "mime_type", "") if document else ""
|
||||||
|
return mime_type.startswith("image/")
|
||||||
|
|
||||||
|
async def handle_forwarded_qr_import(event, raw_text: str) -> str:
|
||||||
|
message = event.message
|
||||||
|
if not message.fwd_from:
|
||||||
|
return "❌ Please forward the original GroupHelp QR image message. Do not upload or paste it manually."
|
||||||
|
if not is_importbackup_body(raw_text):
|
||||||
|
return "❌ Forwarded QR message must have `.importbackup` as its caption/body."
|
||||||
|
if not message.media or not has_image_media(message):
|
||||||
|
return "❌ Forwarded `.importbackup` message must contain a QR image."
|
||||||
|
|
||||||
|
image_bytes = await client.download_media(message, file=bytes)
|
||||||
|
if not image_bytes:
|
||||||
|
return "❌ Could not download the forwarded QR image."
|
||||||
|
|
||||||
|
return await admin_handler.handle_set_qr_backup_from_image(event.chat_id, image_bytes)
|
||||||
|
|
||||||
@client.on(events.NewMessage())
|
@client.on(events.NewMessage())
|
||||||
async def message_handler(event):
|
async def message_handler(event):
|
||||||
"""Handle incoming messages and route to appropriate handlers"""
|
"""Handle incoming messages and route to appropriate handlers"""
|
||||||
message = event.message
|
message = event.message
|
||||||
text = message.text or ""
|
text = getattr(message, "raw_text", None) or getattr(message, "text", None) or getattr(message, "message", "") or ""
|
||||||
chat_id = event.chat_id
|
chat_id = event.chat_id
|
||||||
sender_id = event.sender_id
|
sender_id = event.sender_id
|
||||||
|
|
||||||
@@ -100,13 +127,31 @@ async def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
if lower_text == '!cancel':
|
if lower_text == '!cancel':
|
||||||
|
canceled = False
|
||||||
if sender_id in group_conversations:
|
if sender_id in group_conversations:
|
||||||
del group_conversations[sender_id]
|
del group_conversations[sender_id]
|
||||||
|
canceled = True
|
||||||
|
if sender_id in qr_import_sessions:
|
||||||
|
del qr_import_sessions[sender_id]
|
||||||
|
canceled = True
|
||||||
|
|
||||||
|
if canceled:
|
||||||
await event.respond('🛑 Operation canceled.')
|
await event.respond('🛑 Operation canceled.')
|
||||||
else:
|
else:
|
||||||
await event.respond('❓ No active operation to cancel.')
|
await event.respond('❓ No active operation to cancel.')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if sender_id in qr_import_sessions and not lower_text.startswith('/admin_set_qr'):
|
||||||
|
session = qr_import_sessions[sender_id]
|
||||||
|
if session["chat_id"] != chat_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
response = await handle_forwarded_qr_import(event, raw_text)
|
||||||
|
if response.startswith("✅"):
|
||||||
|
qr_import_sessions.pop(sender_id, None)
|
||||||
|
await event.respond(response)
|
||||||
|
return
|
||||||
|
|
||||||
if lower_text.startswith('!newgrp'):
|
if lower_text.startswith('!newgrp'):
|
||||||
if lower_text != '!newgrp':
|
if lower_text != '!newgrp':
|
||||||
cmd_args = raw_text[len('!newgrp'):].strip()
|
cmd_args = raw_text[len('!newgrp'):].strip()
|
||||||
@@ -186,43 +231,28 @@ async def main():
|
|||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
|
|
||||||
elif text.startswith('/admin_set_users'):
|
elif text.startswith('/admin_set_users'):
|
||||||
# Parse user IDs from command
|
|
||||||
parts = text.split()
|
parts = text.split()
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
try:
|
response = await admin_handler.handle_set_default_users(chat_id, parts[1:])
|
||||||
user_ids = [int(uid) for uid in parts[1:]]
|
await event.respond(response)
|
||||||
response = await admin_handler.handle_set_default_users(chat_id, user_ids)
|
|
||||||
await event.respond(response)
|
|
||||||
except ValueError:
|
|
||||||
await event.respond("❌ Invalid user IDs. Please provide numeric IDs.\n\nUsage: `/admin_set_users <user_id1> <user_id2> ...`")
|
|
||||||
else:
|
else:
|
||||||
await event.respond("❌ Please provide at least one user ID.\n\nUsage: `/admin_set_users <user_id1> <user_id2> ...`")
|
await event.respond("❌ Please provide at least one user ID or username.\n\nUsage: `/admin_set_users <id_or_username1> <id_or_username2> ...`")
|
||||||
|
|
||||||
elif text.startswith('/admin_add_users'):
|
elif text.startswith('/admin_add_users'):
|
||||||
# Parse user IDs from command
|
|
||||||
parts = text.split()
|
parts = text.split()
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
try:
|
response = await admin_handler.handle_add_to_default_users(chat_id, parts[1:])
|
||||||
user_ids = [int(uid) for uid in parts[1:]]
|
await event.respond(response)
|
||||||
response = await admin_handler.handle_add_to_default_users(chat_id, user_ids)
|
|
||||||
await event.respond(response)
|
|
||||||
except ValueError:
|
|
||||||
await event.respond("❌ Invalid user IDs. Please provide numeric IDs.\n\nUsage: `/admin_add_users <user_id1> <user_id2> ...`")
|
|
||||||
else:
|
else:
|
||||||
await event.respond("❌ Please provide at least one user ID.\n\nUsage: `/admin_add_users <user_id1> <user_id2> ...`")
|
await event.respond("❌ Please provide at least one user ID or username.\n\nUsage: `/admin_add_users <id_or_username1> <id_or_username2> ...`")
|
||||||
|
|
||||||
elif text.startswith('/admin_remove_users'):
|
elif text.startswith('/admin_remove_users'):
|
||||||
# Parse user IDs from command
|
|
||||||
parts = text.split()
|
parts = text.split()
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
try:
|
response = await admin_handler.handle_remove_from_default_users(chat_id, parts[1:])
|
||||||
user_ids = [int(uid) for uid in parts[1:]]
|
await event.respond(response)
|
||||||
response = await admin_handler.handle_remove_from_default_users(chat_id, user_ids)
|
|
||||||
await event.respond(response)
|
|
||||||
except ValueError:
|
|
||||||
await event.respond("❌ Invalid user IDs. Please provide numeric IDs.\n\nUsage: `/admin_remove_users <user_id1> <user_id2> ...`")
|
|
||||||
else:
|
else:
|
||||||
await event.respond("❌ Please provide at least one user ID.\n\nUsage: `/admin_remove_users <user_id1> <user_id2> ...`")
|
await event.respond("❌ Please provide at least one user ID or username.\n\nUsage: `/admin_remove_users <id_or_username1> <id_or_username2> ...`")
|
||||||
|
|
||||||
elif text.startswith('/admin_add_user'):
|
elif text.startswith('/admin_add_user'):
|
||||||
# Parse username from command
|
# Parse username from command
|
||||||
@@ -239,14 +269,23 @@ async def main():
|
|||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
|
|
||||||
elif text.startswith('/admin_set_qr'):
|
elif text.startswith('/admin_set_qr'):
|
||||||
# Parse QR data from command
|
|
||||||
parts = text.split(maxsplit=1)
|
parts = text.split(maxsplit=1)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
qr_data = parts[1].strip()
|
qr_data = parts[1].strip()
|
||||||
response = await admin_handler.handle_set_qr_backup(chat_id, qr_data)
|
response = await admin_handler.handle_set_qr_backup(chat_id, qr_data)
|
||||||
|
qr_import_sessions.pop(sender_id, None)
|
||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
else:
|
else:
|
||||||
await event.respond("❌ Please provide QR backup data.\n\nUsage: `/admin_set_qr <qr_code>`")
|
is_admin, error = admin_handler.verify_access(chat_id)
|
||||||
|
if not is_admin:
|
||||||
|
await event.respond(error)
|
||||||
|
else:
|
||||||
|
qr_import_sessions[sender_id] = {"chat_id": chat_id}
|
||||||
|
await event.respond(
|
||||||
|
"📷 Forward the original GroupHelp QR image message here.\n\n"
|
||||||
|
"It must be a forwarded image message with `.importbackup` as its caption/body. "
|
||||||
|
"Send `!cancel` to abort."
|
||||||
|
)
|
||||||
|
|
||||||
elif text.startswith('/admin_help'):
|
elif text.startswith('/admin_help'):
|
||||||
response = await admin_handler.handle_admin_help(chat_id)
|
response = await admin_handler.handle_admin_help(chat_id)
|
||||||
@@ -342,12 +381,13 @@ async def main():
|
|||||||
|
|
||||||
**Admin Commands (Admin Chat Only):**
|
**Admin Commands (Admin Chat Only):**
|
||||||
• `/admin_get_users` - Show default group users
|
• `/admin_get_users` - Show default group users
|
||||||
• `/admin_set_users <id1> <id2>` - Set default users
|
• `/admin_set_users <id_or_username> ...` - Set default users
|
||||||
• `/admin_add_users <id1> <id2>` - Add users to default list
|
• `/admin_add_users <id_or_username> ...` - Add users to default list
|
||||||
• `/admin_remove_users <id1> <id2>` - Remove users from default list
|
• `/admin_remove_users <id_or_username> ...` - Remove users from default list
|
||||||
• `/admin_add_user <username>` - Add new user to database
|
• `/admin_add_user <username>` - Add new user to database
|
||||||
• `/admin_get_qr` - Get QR backup data
|
• `/admin_get_qr` - Get QR backup data
|
||||||
• `/admin_set_qr <data>` - Set QR backup data
|
• `/admin_set_qr <qr_payload>` - Set QR backup data directly
|
||||||
|
• `/admin_set_qr` - Decode a forwarded `.importbackup` QR image
|
||||||
• `/admin_help` - Show admin command help"""
|
• `/admin_help` - Show admin command help"""
|
||||||
await event.respond(help_text)
|
await event.respond(help_text)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from telethon.tl.functions.channels import CreateChannelRequest, InviteToChannel
|
|||||||
|
|
||||||
from src.services.mongodb_service import MongoDBService
|
from src.services.mongodb_service import MongoDBService
|
||||||
from src.config import get_default_group_users, get_qr_data
|
from src.config import get_default_group_users, get_qr_data
|
||||||
|
from src.utils.qr_backup import build_qr_image
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -92,12 +93,6 @@ class GroupService:
|
|||||||
logger.warning(f"Failed to export invite link: {e}")
|
logger.warning(f"Failed to export invite link: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _build_importbackup_message(self, qr_data: str) -> str:
|
|
||||||
qr_data = qr_data.strip()
|
|
||||||
if qr_data.lower().startswith((".importbackup", "/importbackup")):
|
|
||||||
return qr_data
|
|
||||||
return f".importbackup {qr_data}"
|
|
||||||
|
|
||||||
async def create_group(
|
async def create_group(
|
||||||
self,
|
self,
|
||||||
group_name: str,
|
group_name: str,
|
||||||
@@ -220,16 +215,19 @@ class GroupService:
|
|||||||
qr_imported = False
|
qr_imported = False
|
||||||
qr_data = get_qr_data()
|
qr_data = get_qr_data()
|
||||||
if qr_data:
|
if qr_data:
|
||||||
logger.info("Sending GroupHelp QR backup import command")
|
logger.info("Sending GroupHelp QR backup import image")
|
||||||
await self.client.send_message(
|
qr_image = build_qr_image(qr_data)
|
||||||
|
await self.client.send_file(
|
||||||
target_group,
|
target_group,
|
||||||
self._build_importbackup_message(qr_data),
|
qr_image,
|
||||||
|
caption=".importbackup",
|
||||||
|
force_document=False,
|
||||||
)
|
)
|
||||||
qr_imported = True
|
qr_imported = True
|
||||||
if staff_chat_id:
|
if staff_chat_id:
|
||||||
await self.client.send_message(staff_chat_id, "✅ GroupHelp QR backup import sent to the new group.")
|
await self.client.send_message(staff_chat_id, "✅ GroupHelp QR backup image sent to the new group.")
|
||||||
elif staff_chat_id:
|
elif staff_chat_id:
|
||||||
await self.client.send_message(staff_chat_id, "⚠️ No GroupHelp QR backup data configured. Use `/admin_set_qr <qr_data>`.")
|
await self.client.send_message(staff_chat_id, "⚠️ No GroupHelp QR backup data configured. Use `/admin_set_qr <qr_payload>`.")
|
||||||
|
|
||||||
invite_link = await self._export_invite_link(target_group)
|
invite_link = await self._export_invite_link(target_group)
|
||||||
if staff_chat_id:
|
if staff_chat_id:
|
||||||
|
|||||||
1
src/utils/__init__.py
Normal file
1
src/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
77
src/utils/qr_backup.py
Normal file
77
src/utils/qr_backup.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_qr_payload(qr_data: str) -> str:
|
||||||
|
payload = (qr_data or "").strip()
|
||||||
|
lower_payload = payload.lower()
|
||||||
|
for command in (".importbackup", "/importbackup"):
|
||||||
|
if lower_payload.startswith(command):
|
||||||
|
return payload[len(command):].strip()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def build_qr_image(qr_data: str) -> io.BytesIO:
|
||||||
|
payload = normalize_qr_payload(qr_data)
|
||||||
|
if not payload:
|
||||||
|
raise ValueError("QR backup payload is empty")
|
||||||
|
|
||||||
|
import qrcode
|
||||||
|
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=None,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_M,
|
||||||
|
box_size=10,
|
||||||
|
border=4,
|
||||||
|
)
|
||||||
|
qr.add_data(payload)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
image = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
output = io.BytesIO()
|
||||||
|
image.save(output, format="PNG")
|
||||||
|
output.name = "grouphelp-importbackup.png"
|
||||||
|
output.seek(0)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def decode_qr_image_payload(image_bytes: bytes) -> str:
|
||||||
|
if not image_bytes:
|
||||||
|
raise ValueError("QR image is empty")
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
|
try:
|
||||||
|
image = ImageOps.exif_transpose(Image.open(io.BytesIO(image_bytes))).convert("RGB")
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Could not open image: {e}") from e
|
||||||
|
|
||||||
|
detector = cv2.QRCodeDetector()
|
||||||
|
rgb = np.asarray(image)
|
||||||
|
bgr = cv2.cvtColor(rgb, cv2.COLOR_RGB2BGR)
|
||||||
|
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
||||||
|
|
||||||
|
candidates = [bgr, gray]
|
||||||
|
height, width = gray.shape[:2]
|
||||||
|
if min(height, width) < 900:
|
||||||
|
scale = max(2, int(900 / max(1, min(height, width))))
|
||||||
|
candidates.extend([
|
||||||
|
cv2.resize(bgr, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC),
|
||||||
|
cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC),
|
||||||
|
])
|
||||||
|
|
||||||
|
for candidate in candidates:
|
||||||
|
data, _, _ = detector.detectAndDecode(candidate)
|
||||||
|
payload = normalize_qr_payload(data)
|
||||||
|
if payload:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
ok, decoded, _, _ = detector.detectAndDecodeMulti(candidate)
|
||||||
|
if ok:
|
||||||
|
for item in decoded:
|
||||||
|
payload = normalize_qr_payload(item)
|
||||||
|
if payload:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
raise ValueError("No QR code could be decoded from the image")
|
||||||
Reference in New Issue
Block a user