Added grouping
This commit is contained in:
@@ -18,20 +18,27 @@ The following commands can ONLY be run from the admin chat:
|
||||
|
||||
#### User Management
|
||||
```
|
||||
/admin_add_user <username> - Add new user to database
|
||||
/admin_add_user <username_or_id:username> - Add new user to database
|
||||
/admin_add_user - Add a forwarded user to database
|
||||
/admin_get_users - View default users list
|
||||
/admin_set_users <id_or_username> ... - Replace entire default users list
|
||||
/admin_add_users <id_or_username> ... - Add users to default list
|
||||
/admin_set_users <id_or_username_or_id:username> ... - Replace entire default users list
|
||||
/admin_add_users <id_or_username_or_id:username> ... - Add users to default list
|
||||
/admin_add_users - Add a forwarded user to default list
|
||||
/admin_remove_users <id_or_username> - Remove users from default list
|
||||
/admin_remove_users <id_or_username_or_id:username> - Remove users from default list
|
||||
```
|
||||
|
||||
#### GroupHelp QR Code Backup
|
||||
```
|
||||
/admin_get_qr - Retrieve GroupHelp backup QR data
|
||||
/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
|
||||
/admin_get_qr <qr_group> - Retrieve GroupHelp backup QR data for a named config
|
||||
/admin_set_qr <qr_payload> - Store default GroupHelp backup payload
|
||||
/admin_set_qr <qr_group> <payload> - Store named GroupHelp backup payload
|
||||
/admin_set_qr - Wait for a forwarded `.importbackup` QR image for default
|
||||
/admin_set_qr_group <qr_group> - Wait for a forwarded `.importbackup` QR image for a named config
|
||||
/admin_qr_groups [qr_group] - List QR configs and assigned Telegram groups
|
||||
/admin_qr_group_add <qr_group> <group_id> ... - Assign groups to QR config
|
||||
/admin_qr_group_remove <group_id> ... - Remove QR config assignment
|
||||
/admin_sync_qr [qr_group|all] - Send stored `.importbackup` QR to owned assigned groups
|
||||
```
|
||||
|
||||
#### Help
|
||||
@@ -121,12 +128,12 @@ Bot: ✅ Default users updated successfully:
|
||||
• bob (ID: 0987654321)
|
||||
|
||||
Admin: /admin_set_qr 0001a8ac0123456789abcdef...
|
||||
Bot: ✅ QR backup data updated successfully!
|
||||
Bot: ✅ QR backup data for `default` updated successfully!
|
||||
|
||||
Admin: /admin_set_qr
|
||||
Bot: 📷 Forward the original GroupHelp QR image message here.
|
||||
Bot: 📷 Forward the original GroupHelp QR image message for `default` here.
|
||||
Admin: forwards original `.importbackup` QR image message
|
||||
Bot: ✅ QR backup image decoded and stored successfully!
|
||||
Bot: ✅ QR backup image for `default` decoded and stored successfully!
|
||||
```
|
||||
|
||||
### User Usage (any chat)
|
||||
@@ -224,10 +231,10 @@ MONGODB_COLLECTION=ghconfig
|
||||
```bash
|
||||
# In admin chat:
|
||||
/admin_set_qr new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||
# ✅ QR backup data updated successfully!
|
||||
# ✅ QR backup data for `default` updated successfully!
|
||||
|
||||
/admin_get_qr
|
||||
# 📊 Current QR Backup Data: new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||
# 📊 Current QR Backup Data for `default`: new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||
|
||||
# During !newgrp, GroupFactory sends the payload as a QR image with:
|
||||
.importbackup
|
||||
|
||||
@@ -133,6 +133,8 @@ This will create ProjectBeta with only alice and bob, regardless of the default
|
||||
|
||||
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.
|
||||
|
||||
GroupFactory supports multiple logical QR config groups. The built-in group is `default`; additional names such as `sales`, `support`, or `alerts` can each have their own stored QR payload and assigned Telegram groups.
|
||||
|
||||
### Step 1: Generate or Obtain QR Code
|
||||
|
||||
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.
|
||||
@@ -145,6 +147,12 @@ Store the GroupHelp QR backup payload directly:
|
||||
/admin_set_qr YOUR_QR_PAYLOAD_HERE
|
||||
```
|
||||
|
||||
Store a payload for a named logical QR group:
|
||||
|
||||
```
|
||||
/admin_set_qr sales YOUR_SALES_QR_PAYLOAD_HERE
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
/admin_set_qr new_390530c569ec8902a6cbd8482d51d70d2857d4a0
|
||||
@@ -156,11 +164,17 @@ 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.
|
||||
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 for `default`.
|
||||
|
||||
For a named QR group, start the importer with:
|
||||
|
||||
```
|
||||
/admin_set_qr_group sales
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
✅ QR backup data updated successfully!
|
||||
✅ QR backup image for `sales` decoded and stored successfully!
|
||||
|
||||
Data: `new_390530c569ec8902a6cbd8482d51d70d2857d4a0`
|
||||
```
|
||||
@@ -173,9 +187,15 @@ Retrieve the stored GroupHelp QR backup data anytime:
|
||||
/admin_get_qr
|
||||
```
|
||||
|
||||
Retrieve a named QR group:
|
||||
|
||||
```
|
||||
/admin_get_qr sales
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
📊 Current QR Backup Data:
|
||||
📊 Current QR Backup Data for `sales`:
|
||||
`new_390530c569ec8902a6cbd8482d51d70d2857d4a0`
|
||||
```
|
||||
|
||||
@@ -186,6 +206,28 @@ Once you have the QR backup data stored, you can:
|
||||
2. Let GroupFactory render it as a QR image during `!newgrp`
|
||||
3. Let GroupHelp import the backed-up settings from the `.importbackup` image message
|
||||
|
||||
To resync the stored GroupHelp configuration across existing managed groups, run:
|
||||
|
||||
```
|
||||
/admin_sync_qr
|
||||
```
|
||||
|
||||
This sends the `default` stored `.importbackup` QR image only to unassigned owned group dialogs. It skips private chats and channels, and waits 30 seconds between each group.
|
||||
|
||||
Assign Telegram groups to named QR configs:
|
||||
|
||||
```
|
||||
/admin_qr_group_add sales -1001234567890 -1009876543210
|
||||
/admin_qr_groups
|
||||
```
|
||||
|
||||
Sync one named QR group, or all logical groups sequentially:
|
||||
|
||||
```
|
||||
/admin_sync_qr sales
|
||||
/admin_sync_qr all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Structure
|
||||
@@ -318,8 +360,15 @@ FACTORY_BOT_USERNAME=your_bot_username
|
||||
- `/add_default_users <id1> <id2> ...` - Add users to default list
|
||||
- `/remove_default_users <id1> <id2> ...` - Remove users from default list
|
||||
- `/admin_get_qr` - View GroupHelp QR backup data
|
||||
- `/admin_set_qr <qr_payload>` - Set GroupHelp QR backup data directly
|
||||
- `/admin_set_qr` - Decode a forwarded `.importbackup` QR image
|
||||
- `/admin_get_qr <qr_group>` - View GroupHelp QR backup data for a named QR group
|
||||
- `/admin_set_qr <qr_payload>` - Set default GroupHelp QR backup data directly
|
||||
- `/admin_set_qr <qr_group> <qr_payload>` - Set named GroupHelp QR backup data directly
|
||||
- `/admin_set_qr` - Decode a forwarded `.importbackup` QR image for `default`
|
||||
- `/admin_set_qr_group <qr_group>` - Decode a forwarded `.importbackup` QR image for a named QR group
|
||||
- `/admin_qr_groups [qr_group]` - List QR configs and assigned Telegram groups
|
||||
- `/admin_qr_group_add <qr_group> <telegram_group_id> ...` - Assign Telegram groups to a QR config
|
||||
- `/admin_qr_group_remove <telegram_group_id> ...` - Remove Telegram group QR config assignment
|
||||
- `/admin_sync_qr [qr_group|all]` - Send stored `.importbackup` QR to owned assigned groups
|
||||
- `/config_help` - Show configuration command help
|
||||
- `/help` - Show all available commands
|
||||
|
||||
@@ -364,6 +413,12 @@ db.group_config.find()
|
||||
|
||||
# View GroupHelp QR backup
|
||||
db.ghconfig.find({key: 'qr_backup_data'})
|
||||
|
||||
# View all named GroupHelp QR backups
|
||||
db.ghconfig.find({key: /^qr_backup_data/})
|
||||
|
||||
# View QR group assignments
|
||||
db.ghconfig.find({key: 'qr_group_assignments'})
|
||||
```
|
||||
|
||||
---
|
||||
@@ -371,7 +426,7 @@ db.ghconfig.find({key: 'qr_backup_data'})
|
||||
## Operational Notes
|
||||
|
||||
1. QR backup data is a GroupHelp configuration payload, not a Telegram or Telethon session.
|
||||
2. During `!newgrp`, GroupFactory sends `.importbackup` with the stored GroupHelp settings as a QR image.
|
||||
2. During `!newgrp`, GroupFactory sends `.importbackup` with the `default` stored GroupHelp settings as a QR image.
|
||||
3. Limit configuration commands to trusted chats so group settings are changed intentionally.
|
||||
4. Monitor command usage if multiple operators manage the same groups.
|
||||
|
||||
|
||||
19
README.md
19
README.md
@@ -87,16 +87,21 @@ docker run telegram-groupfactory
|
||||
|
||||
### Admin Commands (Admin Chat Only)
|
||||
```
|
||||
/admin_add_user <username> - Add user to database
|
||||
/admin_add_user <username_or_id:username> - Add user to database
|
||||
/admin_add_user - Add a forwarded user to database
|
||||
/admin_set_users <id_or_username> ... - Set default users for groups
|
||||
/admin_add_users <id_or_username> ... - Add users to default list
|
||||
/admin_set_users <id_or_username_or_id:username> ... - Set default users for groups
|
||||
/admin_add_users <id_or_username_or_id:username> ... - Add users to default list
|
||||
/admin_add_users - Add a forwarded user to default list
|
||||
/admin_remove_users <id_or_username> - Remove users from default list
|
||||
/admin_remove_users <id_or_username_or_id:username> - Remove users from default list
|
||||
/admin_get_users - Show current default users
|
||||
/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 [qr_group] - Retrieve GroupHelp backup QR data
|
||||
/admin_set_qr [qr_group] <payload> - Store GroupHelp backup payload
|
||||
/admin_set_qr - Decode forwarded QR image for default config
|
||||
/admin_set_qr_group <qr_group> - Decode forwarded QR image for named config
|
||||
/admin_qr_groups [qr_group] - List QR configs and assigned Telegram groups
|
||||
/admin_qr_group_add <qr_group> <group_id> ... - Assign groups to QR config
|
||||
/admin_qr_group_remove <group_id> ... - Remove QR config assignment
|
||||
/admin_sync_qr [qr_group|all] - Send stored `.importbackup` QR to owned assigned groups
|
||||
```
|
||||
|
||||
### Legacy User Commands
|
||||
|
||||
146
src/config.py
146
src/config.py
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
from pymongo import MongoClient
|
||||
from pymongo.errors import ConnectionFailure
|
||||
from telethon import TelegramClient
|
||||
@@ -19,6 +20,40 @@ MONGODB_COLLECTION = os.environ.get('MONGODB_COLLECTION', '')
|
||||
|
||||
# Logging configuration
|
||||
LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO')
|
||||
DEFAULT_QR_GROUP = "default"
|
||||
QR_ASSIGNMENTS_KEY = "qr_group_assignments"
|
||||
|
||||
def normalize_qr_group_name(group_name: str = None) -> str:
|
||||
"""Normalize the logical QR config group name used in MongoDB keys."""
|
||||
group = (group_name or DEFAULT_QR_GROUP).strip().lower()
|
||||
if not group:
|
||||
group = DEFAULT_QR_GROUP
|
||||
if not re.fullmatch(r"[a-z0-9][a-z0-9_.-]{0,62}", group):
|
||||
raise ValueError("QR group names must use letters, numbers, dot, underscore, or dash")
|
||||
return group
|
||||
|
||||
def _qr_backup_key(group_name: str = None) -> str:
|
||||
group = normalize_qr_group_name(group_name)
|
||||
if group == DEFAULT_QR_GROUP:
|
||||
return "qr_backup_data"
|
||||
return f"qr_backup_data:{group}"
|
||||
|
||||
def _assignment_group(value):
|
||||
if isinstance(value, dict):
|
||||
return normalize_qr_group_name(value.get("group"))
|
||||
return normalize_qr_group_name(value)
|
||||
|
||||
def _assignment_title(value):
|
||||
if isinstance(value, dict):
|
||||
return value.get("title")
|
||||
return None
|
||||
|
||||
def normalize_telegram_group_id(telegram_group_id) -> str:
|
||||
"""Normalize a Telegram group ID used as a MongoDB assignment key."""
|
||||
group_id = str(telegram_group_id).strip().rstrip(",")
|
||||
if not group_id or not group_id.lstrip("-").isdigit():
|
||||
raise ValueError("Telegram group ID must be numeric")
|
||||
return group_id
|
||||
|
||||
def get_telegram_session():
|
||||
"""Return a Telethon session backed by TELETHON_TOKEN when configured."""
|
||||
@@ -50,14 +85,15 @@ def get_telegram_client():
|
||||
print(f"Telegram client creation failed: {e}")
|
||||
return None
|
||||
|
||||
def get_qr_data():
|
||||
"""Retrieve QR data string from MongoDB ghconfig collection"""
|
||||
def get_qr_data(group_name: str = None):
|
||||
"""Retrieve QR data string from MongoDB ghconfig collection."""
|
||||
try:
|
||||
key = _qr_backup_key(group_name)
|
||||
client = get_mongo_client()
|
||||
if client:
|
||||
db = client[MONGODB_DATABASE]
|
||||
collection = db[MONGODB_COLLECTION]
|
||||
result = collection.find_one({'key': 'qr_backup_data'})
|
||||
result = collection.find_one({'key': key})
|
||||
client.close()
|
||||
if result and 'value' in result:
|
||||
return result['value']
|
||||
@@ -123,16 +159,18 @@ def set_default_group_users(user_ids: list):
|
||||
print(f"Failed to set default group users: {e}")
|
||||
return False
|
||||
|
||||
def set_qr_backup_data(qr_data: str):
|
||||
"""Store backup QR code data in MongoDB"""
|
||||
def set_qr_backup_data(qr_data: str, group_name: str = None):
|
||||
"""Store backup QR code data in MongoDB."""
|
||||
try:
|
||||
group = normalize_qr_group_name(group_name)
|
||||
key = _qr_backup_key(group)
|
||||
client = get_mongo_client()
|
||||
if client:
|
||||
db = client[MONGODB_DATABASE]
|
||||
collection = db[MONGODB_COLLECTION]
|
||||
collection.update_one(
|
||||
{'key': 'qr_backup_data'},
|
||||
{'$set': {'value': qr_data}},
|
||||
{'key': key},
|
||||
{'$set': {'value': qr_data, 'group': group}},
|
||||
upsert=True
|
||||
)
|
||||
client.close()
|
||||
@@ -142,6 +180,100 @@ def set_qr_backup_data(qr_data: str):
|
||||
print(f"Failed to set QR backup data: {e}")
|
||||
return False
|
||||
|
||||
def list_qr_groups(include_assignments: bool = True):
|
||||
"""List logical QR config group names known from QR payloads and assignments."""
|
||||
groups = set()
|
||||
try:
|
||||
client = get_mongo_client()
|
||||
if client:
|
||||
db = client[MONGODB_DATABASE]
|
||||
collection = db[MONGODB_COLLECTION]
|
||||
for result in collection.find({'key': {'$regex': r'^qr_backup_data(:.*)?$'}}):
|
||||
key = result.get('key')
|
||||
if key == 'qr_backup_data':
|
||||
groups.add(DEFAULT_QR_GROUP)
|
||||
elif key and key.startswith('qr_backup_data:'):
|
||||
groups.add(normalize_qr_group_name(key.split(':', 1)[1]))
|
||||
if include_assignments:
|
||||
assignment_doc = collection.find_one({'key': QR_ASSIGNMENTS_KEY})
|
||||
assignments = (assignment_doc or {}).get('value') or {}
|
||||
for assignment in assignments.values():
|
||||
groups.add(_assignment_group(assignment))
|
||||
client.close()
|
||||
except Exception as e:
|
||||
print(f"Failed to list QR groups: {e}")
|
||||
return sorted(groups)
|
||||
|
||||
def get_qr_group_assignments(group_name: str = None):
|
||||
"""Return Telegram group-id assignments for logical QR config groups."""
|
||||
try:
|
||||
target_group = normalize_qr_group_name(group_name) if group_name else None
|
||||
client = get_mongo_client()
|
||||
if client:
|
||||
db = client[MONGODB_DATABASE]
|
||||
collection = db[MONGODB_COLLECTION]
|
||||
result = collection.find_one({'key': QR_ASSIGNMENTS_KEY})
|
||||
client.close()
|
||||
assignments = (result or {}).get('value') or {}
|
||||
normalized = {}
|
||||
for telegram_group_id, assignment in assignments.items():
|
||||
assignment_group = _assignment_group(assignment)
|
||||
if target_group and assignment_group != target_group:
|
||||
continue
|
||||
normalized[str(telegram_group_id)] = {
|
||||
'group': assignment_group,
|
||||
'title': _assignment_title(assignment),
|
||||
}
|
||||
return normalized
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Failed to retrieve QR group assignments: {e}")
|
||||
return {}
|
||||
|
||||
def set_qr_group_assignment(group_name: str, telegram_group_id, title: str = None):
|
||||
"""Assign a Telegram group to a logical QR config group."""
|
||||
try:
|
||||
group = normalize_qr_group_name(group_name)
|
||||
group_id = normalize_telegram_group_id(telegram_group_id)
|
||||
|
||||
client = get_mongo_client()
|
||||
if client:
|
||||
db = client[MONGODB_DATABASE]
|
||||
collection = db[MONGODB_COLLECTION]
|
||||
value = {'group': group}
|
||||
if title:
|
||||
value['title'] = title
|
||||
collection.update_one(
|
||||
{'key': QR_ASSIGNMENTS_KEY},
|
||||
{'$set': {f'value.{group_id}': value}},
|
||||
upsert=True
|
||||
)
|
||||
client.close()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to set QR group assignment: {e}")
|
||||
return False
|
||||
|
||||
def remove_qr_group_assignment(telegram_group_id):
|
||||
"""Remove any logical QR config assignment for a Telegram group."""
|
||||
try:
|
||||
group_id = normalize_telegram_group_id(telegram_group_id)
|
||||
client = get_mongo_client()
|
||||
if client:
|
||||
db = client[MONGODB_DATABASE]
|
||||
collection = db[MONGODB_COLLECTION]
|
||||
result = collection.update_one(
|
||||
{'key': QR_ASSIGNMENTS_KEY},
|
||||
{'$unset': {f'value.{group_id}': ""}},
|
||||
)
|
||||
client.close()
|
||||
return result.modified_count > 0
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to remove QR group assignment: {e}")
|
||||
return False
|
||||
|
||||
def is_admin_chat(chat_id: int) -> bool:
|
||||
"""Verify if the message came from the admin chat"""
|
||||
return chat_id == STAFF_CHAT_ID
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import List
|
||||
from src.config import (
|
||||
get_default_group_users, set_default_group_users, get_qr_data,
|
||||
set_qr_backup_data, verify_admin_access
|
||||
set_qr_backup_data, verify_admin_access, DEFAULT_QR_GROUP,
|
||||
normalize_qr_group_name, list_qr_groups, get_qr_group_assignments,
|
||||
normalize_telegram_group_id, set_qr_group_assignment,
|
||||
remove_qr_group_assignment
|
||||
)
|
||||
from src.services.user_service import UserService
|
||||
from src.models.user import User
|
||||
@@ -63,6 +67,12 @@ class AdminHandler:
|
||||
def _is_numeric_id(self, value) -> bool:
|
||||
return str(value).strip().lstrip("-").isdigit()
|
||||
|
||||
def _parse_id_username_pair(self, identifier: str):
|
||||
match = re.fullmatch(r"(-?\d+)\s*[:=,|]\s*@?([A-Za-z0-9_]{5,32})", identifier)
|
||||
if not match:
|
||||
return None
|
||||
return int(match.group(1)), match.group(2)
|
||||
|
||||
def _user_from_entity(self, entity, fallback_username: str = None) -> User:
|
||||
username = getattr(entity, "username", None) or fallback_username
|
||||
return User(
|
||||
@@ -85,6 +95,41 @@ class AdminHandler:
|
||||
if not identifier:
|
||||
raise ValueError("Empty user identifier")
|
||||
|
||||
id_username_pair = self._parse_id_username_pair(identifier)
|
||||
if id_username_pair:
|
||||
provided_user_id, provided_username = id_username_pair
|
||||
existing = self.user_service.get_user_by_id(provided_user_id)
|
||||
created = existing is None
|
||||
|
||||
user = User(
|
||||
id=provided_user_id,
|
||||
username=provided_username,
|
||||
first_name=existing.first_name if existing else None,
|
||||
last_name=existing.last_name if existing else None,
|
||||
access_hash=existing.access_hash if existing else None,
|
||||
)
|
||||
|
||||
if self.telegram_client:
|
||||
try:
|
||||
entity = await self.telegram_client.get_entity(provided_username)
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
"Could not resolve username @%s while storing ID pair %s: %s",
|
||||
provided_username,
|
||||
provided_user_id,
|
||||
e,
|
||||
)
|
||||
else:
|
||||
if entity.id != provided_user_id:
|
||||
raise ValueError(
|
||||
f"@{provided_username} resolves to Telegram ID {entity.id}, not {provided_user_id}"
|
||||
)
|
||||
user = self._user_from_entity(entity, fallback_username=provided_username)
|
||||
|
||||
if not self.user_service.save_user(user):
|
||||
raise RuntimeError(f"Failed to save user {provided_user_id}")
|
||||
return provided_user_id, user, created
|
||||
|
||||
if self._is_numeric_id(identifier):
|
||||
user_id = int(identifier)
|
||||
if self.telegram_client:
|
||||
@@ -123,6 +168,9 @@ class AdminHandler:
|
||||
def verify_access(self, chat_id: int) -> tuple:
|
||||
"""Verify admin access"""
|
||||
return verify_admin_access(chat_id)
|
||||
|
||||
def _qr_group_label(self, group_name: str = None) -> str:
|
||||
return normalize_qr_group_name(group_name or DEFAULT_QR_GROUP)
|
||||
|
||||
async def handle_get_default_users(self, chat_id: int) -> str:
|
||||
"""Get current default users for new groups (Admin only)"""
|
||||
@@ -133,7 +181,7 @@ class AdminHandler:
|
||||
try:
|
||||
default_users = get_default_group_users()
|
||||
if not default_users:
|
||||
return "📋 No default users configured yet.\n\nUse `/admin_set_users <id_or_username1> <id_or_username2> ...` to configure them."
|
||||
return "📋 No default users configured yet.\n\nUse `/admin_set_users <id_or_username_or_id:username> ...` to configure them."
|
||||
|
||||
user_list = []
|
||||
for user_id in default_users:
|
||||
@@ -156,7 +204,7 @@ class AdminHandler:
|
||||
|
||||
try:
|
||||
if not user_ids:
|
||||
return "❌ Please provide at least one user ID or username.\n\nUsage: `/admin_set_users <id_or_username1> <id_or_username2> ...`"
|
||||
return "❌ Please provide at least one user ID, username, or id:username pair.\n\nUsage: `/admin_set_users <id_or_username_or_id:username> ...`"
|
||||
|
||||
valid_user_ids, created_users, errors = await self._resolve_user_identifiers(user_ids)
|
||||
if not valid_user_ids:
|
||||
@@ -192,7 +240,7 @@ class AdminHandler:
|
||||
|
||||
try:
|
||||
if not user_ids:
|
||||
return "❌ Please provide at least one user ID or username.\n\nUsage: `/admin_add_users <id_or_username1> <id_or_username2> ...`"
|
||||
return "❌ Please provide at least one user ID, username, or id:username pair.\n\nUsage: `/admin_add_users <id_or_username_or_id:username> ...`"
|
||||
|
||||
current_users = get_default_group_users()
|
||||
|
||||
@@ -236,7 +284,7 @@ class AdminHandler:
|
||||
|
||||
try:
|
||||
if not user_ids:
|
||||
return "❌ Please provide at least one user ID or username.\n\nUsage: `/admin_remove_users <id_or_username1> <id_or_username2> ...`"
|
||||
return "❌ Please provide at least one user ID, username, or id:username pair.\n\nUsage: `/admin_remove_users <id_or_username_or_id:username> ...`"
|
||||
|
||||
current_users = get_default_group_users()
|
||||
resolved_user_ids, _, errors = await self._resolve_user_identifiers(user_ids)
|
||||
@@ -280,7 +328,7 @@ class AdminHandler:
|
||||
|
||||
try:
|
||||
if not username or len(username.strip()) == 0:
|
||||
return "❌ Please provide a username.\n\nUsage: `/admin_add_user <username>`"
|
||||
return "❌ Please provide a username or id:username pair.\n\nUsage: `/admin_add_user <username_or_id:username>`"
|
||||
|
||||
user_id, user, created = await self._resolve_user_identifier(username)
|
||||
action = "added" if created else "updated"
|
||||
@@ -317,63 +365,176 @@ class AdminHandler:
|
||||
logger.error(f"Error adding forwarded user entity: {e}")
|
||||
return f"❌ Error adding forwarded user: {str(e)}"
|
||||
|
||||
async def handle_get_qr_backup(self, chat_id: int) -> str:
|
||||
async def handle_get_qr_backup(self, chat_id: int, qr_group: str = None) -> str:
|
||||
"""Retrieve current QR backup data (Admin only)"""
|
||||
is_admin, error = self.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
return error
|
||||
|
||||
try:
|
||||
qr_data = get_qr_data()
|
||||
group = self._qr_group_label(qr_group)
|
||||
qr_data = get_qr_data(group)
|
||||
if qr_data:
|
||||
return f"📊 Current QR Backup Data:\n`{qr_data}`"
|
||||
return f"📊 Current QR Backup Data for `{group}`:\n`{qr_data}`"
|
||||
else:
|
||||
return "📋 No QR backup data configured yet.\n\nUse `/admin_set_qr <qr_payload>` to configure it."
|
||||
return f"📋 No QR backup data configured for `{group}`.\n\nUse `/admin_set_qr {group} <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:
|
||||
def _store_qr_backup_payload(self, qr_data: str, qr_group: str = None) -> tuple:
|
||||
group = self._qr_group_label(qr_group)
|
||||
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
|
||||
return False, "❌ QR backup data cannot be empty.\n\nUsage: `/admin_set_qr [qr_group] <qr_payload>`", None
|
||||
|
||||
if set_qr_backup_data(payload):
|
||||
return True, f"✅ QR backup data updated successfully!\n\nData: `{payload}`", payload
|
||||
if set_qr_backup_data(payload, group):
|
||||
return True, f"✅ QR backup data for `{group}` 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, qr_group: str = None) -> str:
|
||||
"""Set QR backup data (Admin only)"""
|
||||
is_admin, error = self.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
return error
|
||||
|
||||
try:
|
||||
_, message, _ = self._store_qr_backup_payload(qr_data)
|
||||
_, message, _ = self._store_qr_backup_payload(qr_data, qr_group)
|
||||
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:
|
||||
async def handle_set_qr_backup_from_image(self, chat_id: int, image_bytes: bytes, qr_group: str = None) -> 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:
|
||||
group = self._qr_group_label(qr_group)
|
||||
payload = decode_qr_image_payload(image_bytes)
|
||||
saved, message, _ = self._store_qr_backup_payload(payload)
|
||||
saved, message, _ = self._store_qr_backup_payload(payload, group)
|
||||
if saved:
|
||||
return message.replace(
|
||||
"QR backup data updated successfully",
|
||||
"QR backup image decoded and stored successfully",
|
||||
f"QR backup data for `{group}` updated successfully",
|
||||
f"QR backup image for `{group}` 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_assign_qr_group(self, chat_id: int, qr_group: str, group_ids: List[str]) -> str:
|
||||
"""Assign Telegram groups to a logical QR config group."""
|
||||
is_admin, error = self.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
return error
|
||||
|
||||
try:
|
||||
group = self._qr_group_label(qr_group)
|
||||
if not group_ids:
|
||||
return "❌ Please provide at least one Telegram group ID.\n\nUsage: `/admin_qr_group_add <qr_group> <telegram_group_id> ...`"
|
||||
|
||||
assigned = []
|
||||
failed = []
|
||||
invalid = []
|
||||
for raw_group_id in group_ids:
|
||||
try:
|
||||
group_id = normalize_telegram_group_id(raw_group_id)
|
||||
except ValueError:
|
||||
invalid.append(str(raw_group_id))
|
||||
continue
|
||||
|
||||
if set_qr_group_assignment(group, group_id):
|
||||
assigned.append(group_id)
|
||||
else:
|
||||
failed.append(group_id)
|
||||
|
||||
response = f"✅ QR group `{group}` assignments updated."
|
||||
if assigned:
|
||||
response += "\nAssigned groups:\n" + "\n".join([f" • {item}" for item in assigned])
|
||||
if failed:
|
||||
response += "\n\n❌ Failed assignments:\n" + "\n".join([f" • {item}" for item in failed])
|
||||
if invalid:
|
||||
response += "\n\n⚠️ Invalid group IDs:\n" + "\n".join([f" • {item}" for item in invalid])
|
||||
if not get_qr_data(group):
|
||||
response += f"\n\n⚠️ No QR payload is configured for `{group}` yet."
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error assigning QR group {qr_group}: {e}")
|
||||
return f"❌ Error assigning QR group: {str(e)}"
|
||||
|
||||
async def handle_remove_qr_group_assignment(self, chat_id: int, group_ids: List[str]) -> str:
|
||||
"""Remove Telegram group logical QR assignments."""
|
||||
is_admin, error = self.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
return error
|
||||
|
||||
try:
|
||||
if not group_ids:
|
||||
return "❌ Please provide at least one Telegram group ID.\n\nUsage: `/admin_qr_group_remove <telegram_group_id> ...`"
|
||||
|
||||
removed = []
|
||||
missing = []
|
||||
invalid = []
|
||||
for raw_group_id in group_ids:
|
||||
try:
|
||||
group_id = normalize_telegram_group_id(raw_group_id)
|
||||
except ValueError:
|
||||
invalid.append(str(raw_group_id))
|
||||
continue
|
||||
|
||||
if remove_qr_group_assignment(group_id):
|
||||
removed.append(group_id)
|
||||
else:
|
||||
missing.append(group_id)
|
||||
|
||||
response = "✅ QR group assignments updated."
|
||||
if removed:
|
||||
response += "\nRemoved assignments:\n" + "\n".join([f" • {item}" for item in removed])
|
||||
if missing:
|
||||
response += "\n\nℹ️ No assignment existed for:\n" + "\n".join([f" • {item}" for item in missing])
|
||||
if invalid:
|
||||
response += "\n\n⚠️ Invalid group IDs:\n" + "\n".join([f" • {item}" for item in invalid])
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing QR group assignments: {e}")
|
||||
return f"❌ Error removing QR group assignments: {str(e)}"
|
||||
|
||||
async def handle_list_qr_groups(self, chat_id: int, qr_group: str = None) -> str:
|
||||
"""List logical QR config groups and Telegram group assignments."""
|
||||
is_admin, error = self.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
return error
|
||||
|
||||
try:
|
||||
target_group = self._qr_group_label(qr_group) if qr_group else None
|
||||
groups = [target_group] if target_group else list_qr_groups(include_assignments=True)
|
||||
if DEFAULT_QR_GROUP not in groups and not target_group:
|
||||
groups.insert(0, DEFAULT_QR_GROUP)
|
||||
|
||||
assignments = get_qr_group_assignments(target_group)
|
||||
lines = []
|
||||
for group in groups:
|
||||
qr_status = "configured" if get_qr_data(group) else "missing QR"
|
||||
group_assignments = {
|
||||
group_id: item for group_id, item in assignments.items()
|
||||
if item.get('group') == group
|
||||
}
|
||||
lines.append(f"• `{group}` - {qr_status} - {len(group_assignments)} assigned groups")
|
||||
for group_id, item in sorted(group_assignments.items()):
|
||||
title = item.get('title')
|
||||
suffix = f" ({title})" if title else ""
|
||||
lines.append(f" - {group_id}{suffix}")
|
||||
|
||||
if not lines:
|
||||
return "📋 No QR groups configured yet."
|
||||
return "📋 QR config groups:\n" + "\n".join(lines)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing QR groups: {e}")
|
||||
return f"❌ Error listing QR groups: {str(e)}"
|
||||
|
||||
async def handle_admin_help(self, chat_id: int) -> str:
|
||||
"""Show admin command help (Admin only)"""
|
||||
@@ -384,18 +545,23 @@ class AdminHandler:
|
||||
return """🔐 Admin Configuration Commands:
|
||||
|
||||
**Default Group Users Management:**
|
||||
• `/admin_set_users <id_or_username> ...` - Replace entire default users list
|
||||
• `/admin_add_users <id_or_username> ...` - Add users to default list
|
||||
• `/admin_set_users <id_or_username_or_id:username> ...` - Replace entire default users list
|
||||
• `/admin_add_users <id_or_username_or_id:username> ...` - Add users to default list
|
||||
• `/admin_add_users` - Wait for a forwarded user message and add it to defaults
|
||||
• `/admin_remove_users <id_or_username> ...` - Remove users from default list
|
||||
• `/admin_remove_users <id_or_username_or_id:username> ...` - Remove users from default list
|
||||
• `/admin_get_users` - Show current default users
|
||||
• `/admin_add_user <username>` - Resolve and save a user in the database
|
||||
• `/admin_add_user <username_or_id:username>` - Resolve and save a user in the database
|
||||
• `/admin_add_user` - Wait for a forwarded user message and save it
|
||||
|
||||
**QR Code Backup:**
|
||||
• `/admin_get_qr` - Get current QR backup 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
|
||||
• `/admin_get_qr [qr_group]` - Get QR backup data
|
||||
• `/admin_set_qr [qr_group] <qr_payload>` - Set GroupHelp backup payload
|
||||
• `/admin_set_qr` - Wait for a forwarded `.importbackup` QR image for `default`
|
||||
• `/admin_set_qr_group <qr_group>` - Wait for a forwarded `.importbackup` QR image for a QR group
|
||||
• `/admin_qr_groups [qr_group]` - List QR config groups and assignments
|
||||
• `/admin_qr_group_add <qr_group> <telegram_group_id> ...` - Assign groups to a QR config
|
||||
• `/admin_qr_group_remove <telegram_group_id> ...` - Remove QR config assignments
|
||||
• `/admin_sync_qr [qr_group|all]` - Send stored `.importbackup` QR to owned assigned groups
|
||||
|
||||
**Notes:**
|
||||
✓ All admin commands work ONLY from the admin chat
|
||||
|
||||
137
src/main.py
137
src/main.py
@@ -2,7 +2,7 @@ import asyncio
|
||||
import logging
|
||||
import uvicorn
|
||||
from telethon import TelegramClient, events, types
|
||||
from src.config import load_config
|
||||
from src.config import DEFAULT_QR_GROUP, load_config
|
||||
from src.services.mongodb_service import MongoDBService
|
||||
from src.services.user_service import UserService
|
||||
from src.services.group_service import GroupService
|
||||
@@ -54,6 +54,7 @@ async def main():
|
||||
group_conversations = {}
|
||||
qr_import_sessions = {}
|
||||
user_import_sessions = {}
|
||||
background_tasks = {"grouphelp_qr_sync": None}
|
||||
|
||||
async def create_group_from_staff_flow(event, name: str, description: str):
|
||||
response = await group_handler.handle_create_group(
|
||||
@@ -79,7 +80,7 @@ async def main():
|
||||
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:
|
||||
async def handle_forwarded_qr_import(event, raw_text: str, qr_group: str = DEFAULT_QR_GROUP) -> 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."
|
||||
@@ -92,7 +93,7 @@ async def main():
|
||||
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)
|
||||
return await admin_handler.handle_set_qr_backup_from_image(event.chat_id, image_bytes, qr_group=qr_group)
|
||||
|
||||
async def handle_forwarded_user_import(event, add_to_defaults: bool) -> str:
|
||||
message = event.message
|
||||
@@ -175,7 +176,11 @@ async def main():
|
||||
if session["chat_id"] != chat_id:
|
||||
return
|
||||
|
||||
response = await handle_forwarded_qr_import(event, raw_text)
|
||||
response = await handle_forwarded_qr_import(
|
||||
event,
|
||||
raw_text,
|
||||
qr_group=session.get("qr_group", DEFAULT_QR_GROUP),
|
||||
)
|
||||
if response.startswith("✅"):
|
||||
qr_import_sessions.pop(sender_id, None)
|
||||
await event.respond(response)
|
||||
@@ -282,7 +287,7 @@ async def main():
|
||||
response = await admin_handler.handle_set_default_users(chat_id, parts[1:])
|
||||
await event.respond(response)
|
||||
else:
|
||||
await event.respond("❌ Please provide at least one user ID or username.\n\nUsage: `/admin_set_users <id_or_username1> <id_or_username2> ...`")
|
||||
await event.respond("❌ Please provide at least one user ID, username, or id:username pair.\n\nUsage: `/admin_set_users <id_or_username_or_id:username> ...`")
|
||||
|
||||
elif text.startswith('/admin_add_users'):
|
||||
parts = text.split()
|
||||
@@ -312,7 +317,7 @@ async def main():
|
||||
response = await admin_handler.handle_remove_from_default_users(chat_id, parts[1:])
|
||||
await event.respond(response)
|
||||
else:
|
||||
await event.respond("❌ Please provide at least one user ID or username.\n\nUsage: `/admin_remove_users <id_or_username1> <id_or_username2> ...`")
|
||||
await event.respond("❌ Please provide at least one user ID, username, or id:username pair.\n\nUsage: `/admin_remove_users <id_or_username_or_id:username> ...`")
|
||||
|
||||
elif text.startswith('/admin_add_user'):
|
||||
parts = text.split(maxsplit=1)
|
||||
@@ -338,14 +343,101 @@ async def main():
|
||||
)
|
||||
|
||||
elif text.startswith('/admin_get_qr'):
|
||||
response = await admin_handler.handle_get_qr_backup(chat_id)
|
||||
parts = text.split(maxsplit=1)
|
||||
qr_group = parts[1].strip() if len(parts) > 1 else DEFAULT_QR_GROUP
|
||||
response = await admin_handler.handle_get_qr_backup(chat_id, qr_group=qr_group)
|
||||
await event.respond(response)
|
||||
|
||||
elif text.startswith('/admin_set_qr'):
|
||||
|
||||
elif text.startswith('/admin_sync_qr'):
|
||||
is_admin, error = admin_handler.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
await event.respond(error)
|
||||
else:
|
||||
current_task = background_tasks.get("grouphelp_qr_sync")
|
||||
if current_task and not current_task.done():
|
||||
await event.respond("ℹ️ GroupHelp QR sync is already running.")
|
||||
else:
|
||||
parts = text.split(maxsplit=1)
|
||||
sync_group = parts[1].strip().lower() if len(parts) > 1 else DEFAULT_QR_GROUP
|
||||
|
||||
async def send_sync_status(message: str):
|
||||
return await client.send_message(chat_id, message)
|
||||
|
||||
async def run_grouphelp_qr_sync():
|
||||
try:
|
||||
if sync_group == "all":
|
||||
await group_service.sync_all_grouphelp_qr_groups(
|
||||
status_callback=send_sync_status,
|
||||
delay_seconds=30,
|
||||
)
|
||||
else:
|
||||
await group_service.sync_grouphelp_qr_to_owned_groups(
|
||||
status_callback=send_sync_status,
|
||||
delay_seconds=30,
|
||||
qr_group=sync_group,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"GroupHelp QR sync task failed: {e}")
|
||||
await client.send_message(chat_id, f"❌ GroupHelp QR sync failed: {e}")
|
||||
finally:
|
||||
background_tasks["grouphelp_qr_sync"] = None
|
||||
|
||||
background_tasks["grouphelp_qr_sync"] = asyncio.create_task(
|
||||
run_grouphelp_qr_sync(),
|
||||
name="grouphelp-qr-sync",
|
||||
)
|
||||
await event.respond(f"✅ GroupHelp QR sync `{sync_group}` started in background.")
|
||||
|
||||
elif text.startswith('/admin_qr_group_add'):
|
||||
parts = text.split()
|
||||
if len(parts) > 2:
|
||||
response = await admin_handler.handle_assign_qr_group(chat_id, parts[1], parts[2:])
|
||||
await event.respond(response)
|
||||
else:
|
||||
await event.respond("❌ Usage: `/admin_qr_group_add <qr_group> <telegram_group_id> ...`")
|
||||
|
||||
elif text.startswith('/admin_qr_group_remove'):
|
||||
parts = text.split()
|
||||
if len(parts) > 1:
|
||||
response = await admin_handler.handle_remove_qr_group_assignment(chat_id, parts[1:])
|
||||
await event.respond(response)
|
||||
else:
|
||||
await event.respond("❌ Usage: `/admin_qr_group_remove <telegram_group_id> ...`")
|
||||
|
||||
elif text.startswith('/admin_qr_groups'):
|
||||
parts = text.split(maxsplit=1)
|
||||
qr_group = parts[1].strip() if len(parts) > 1 else None
|
||||
response = await admin_handler.handle_list_qr_groups(chat_id, qr_group=qr_group)
|
||||
await event.respond(response)
|
||||
|
||||
elif text.startswith('/admin_set_qr_group'):
|
||||
parts = text.split(maxsplit=1)
|
||||
if len(parts) > 1:
|
||||
qr_data = parts[1].strip()
|
||||
response = await admin_handler.handle_set_qr_backup(chat_id, qr_data)
|
||||
is_admin, error = admin_handler.verify_access(chat_id)
|
||||
if not is_admin:
|
||||
await event.respond(error)
|
||||
else:
|
||||
qr_group = parts[1].strip()
|
||||
qr_import_sessions[sender_id] = {"chat_id": chat_id, "qr_group": qr_group}
|
||||
user_import_sessions.pop(sender_id, None)
|
||||
await event.respond(
|
||||
f"📷 Forward the original GroupHelp QR image message for `{qr_group}` here.\n\n"
|
||||
"It must be a forwarded image message with `.importbackup` as its caption/body. "
|
||||
"Send `!cancel` to abort."
|
||||
)
|
||||
else:
|
||||
await event.respond("❌ Usage: `/admin_set_qr_group <qr_group>`")
|
||||
|
||||
elif text.startswith('/admin_set_qr'):
|
||||
parts = text.split(maxsplit=2)
|
||||
if len(parts) > 1:
|
||||
if len(parts) > 2:
|
||||
qr_group = parts[1].strip()
|
||||
qr_data = parts[2].strip()
|
||||
else:
|
||||
qr_group = DEFAULT_QR_GROUP
|
||||
qr_data = parts[1].strip()
|
||||
response = await admin_handler.handle_set_qr_backup(chat_id, qr_data, qr_group=qr_group)
|
||||
qr_import_sessions.pop(sender_id, None)
|
||||
user_import_sessions.pop(sender_id, None)
|
||||
await event.respond(response)
|
||||
@@ -354,10 +446,10 @@ async def main():
|
||||
if not is_admin:
|
||||
await event.respond(error)
|
||||
else:
|
||||
qr_import_sessions[sender_id] = {"chat_id": chat_id}
|
||||
qr_import_sessions[sender_id] = {"chat_id": chat_id, "qr_group": DEFAULT_QR_GROUP}
|
||||
user_import_sessions.pop(sender_id, None)
|
||||
await event.respond(
|
||||
"📷 Forward the original GroupHelp QR image message here.\n\n"
|
||||
"📷 Forward the original GroupHelp QR image message for `default` here.\n\n"
|
||||
"It must be a forwarded image message with `.importbackup` as its caption/body. "
|
||||
"Send `!cancel` to abort."
|
||||
)
|
||||
@@ -456,15 +548,20 @@ async def main():
|
||||
|
||||
**Admin Commands (Admin Chat Only):**
|
||||
• `/admin_get_users` - Show default group users
|
||||
• `/admin_set_users <id_or_username> ...` - Set default users
|
||||
• `/admin_add_users <id_or_username> ...` - Add users to default list
|
||||
• `/admin_set_users <id_or_username_or_id:username> ...` - Set default users
|
||||
• `/admin_add_users <id_or_username_or_id:username> ...` - Add users to default list
|
||||
• `/admin_add_users` - Add a forwarded user to default list
|
||||
• `/admin_remove_users <id_or_username> ...` - Remove users from default list
|
||||
• `/admin_add_user <username>` - Add new user to database
|
||||
• `/admin_remove_users <id_or_username_or_id:username> ...` - Remove users from default list
|
||||
• `/admin_add_user <username_or_id:username>` - Add new user to database
|
||||
• `/admin_add_user` - Add a forwarded user to database
|
||||
• `/admin_get_qr` - Get QR backup data
|
||||
• `/admin_set_qr <qr_payload>` - Set QR backup data directly
|
||||
• `/admin_set_qr` - Decode a forwarded `.importbackup` QR image
|
||||
• `/admin_get_qr [qr_group]` - Get QR backup data
|
||||
• `/admin_set_qr [qr_group] <qr_payload>` - Set QR backup data directly
|
||||
• `/admin_set_qr` - Decode a forwarded `.importbackup` QR image for `default`
|
||||
• `/admin_set_qr_group <qr_group>` - Decode a forwarded `.importbackup` QR image for a QR group
|
||||
• `/admin_qr_groups [qr_group]` - List QR groups and assignments
|
||||
• `/admin_qr_group_add <qr_group> <telegram_group_id> ...` - Assign groups to QR config
|
||||
• `/admin_qr_group_remove <telegram_group_id> ...` - Remove QR assignments
|
||||
• `/admin_sync_qr [qr_group|all]` - Send stored `.importbackup` QR to owned assigned groups
|
||||
• `/admin_help` - Show admin command help"""
|
||||
await event.respond(help_text)
|
||||
|
||||
|
||||
@@ -7,7 +7,14 @@ from telethon.errors.rpcerrorlist import PeerFloodError, UserPrivacyRestrictedEr
|
||||
from telethon.tl.functions.channels import CreateChannelRequest, InviteToChannelRequest
|
||||
|
||||
from src.services.mongodb_service import MongoDBService
|
||||
from src.config import get_default_group_users, get_qr_data
|
||||
from src.config import (
|
||||
DEFAULT_QR_GROUP,
|
||||
get_default_group_users,
|
||||
get_qr_data,
|
||||
get_qr_group_assignments,
|
||||
list_qr_groups,
|
||||
normalize_qr_group_name,
|
||||
)
|
||||
from src.utils.qr_backup import build_qr_image
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -93,6 +100,30 @@ class GroupService:
|
||||
logger.warning(f"Failed to export invite link: {e}")
|
||||
return None
|
||||
|
||||
def _is_owned_group_dialog(self, dialog) -> bool:
|
||||
entity = getattr(dialog, "entity", None)
|
||||
if not entity or not getattr(dialog, "is_group", False):
|
||||
return False
|
||||
if getattr(entity, "broadcast", False):
|
||||
return False
|
||||
return bool(getattr(entity, "creator", False))
|
||||
|
||||
def _dialog_assignment_keys(self, dialog) -> List[str]:
|
||||
keys = {str(getattr(dialog, "id", ""))}
|
||||
entity = getattr(dialog, "entity", None)
|
||||
entity_id = getattr(entity, "id", None)
|
||||
if entity_id is not None:
|
||||
keys.add(str(entity_id))
|
||||
keys.add(f"-100{entity_id}")
|
||||
return [key for key in keys if key]
|
||||
|
||||
def _dialog_assignment_group(self, dialog, assignments: dict) -> Optional[str]:
|
||||
for key in self._dialog_assignment_keys(dialog):
|
||||
assignment = assignments.get(key)
|
||||
if assignment:
|
||||
return assignment.get("group")
|
||||
return None
|
||||
|
||||
def _user_label(self, user) -> str:
|
||||
if user.username:
|
||||
return f"@{user.username}"
|
||||
@@ -107,12 +138,11 @@ class GroupService:
|
||||
self.mongo_service.save_user(user)
|
||||
|
||||
async def _resolve_input_user(self, user):
|
||||
if user.access_hash:
|
||||
return types.InputUser(user.id, user.access_hash)
|
||||
|
||||
references = []
|
||||
if user.username:
|
||||
references.append(user.username)
|
||||
if user.access_hash:
|
||||
references.append(types.InputUser(user.id, user.access_hash))
|
||||
references.append(user.id)
|
||||
|
||||
last_error = None
|
||||
@@ -128,7 +158,7 @@ class GroupService:
|
||||
except ValueError as e:
|
||||
last_error = e
|
||||
|
||||
if not user.username:
|
||||
if not user.username and not user.access_hash:
|
||||
raise ValueError(
|
||||
"numeric ID has no stored access_hash. Add this user by @username "
|
||||
"or use /admin_add_user or /admin_add_users with no args and "
|
||||
@@ -292,6 +322,148 @@ class GroupService:
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating group '{group_name}': {e}")
|
||||
return None
|
||||
|
||||
async def sync_grouphelp_qr_to_owned_groups(
|
||||
self,
|
||||
status_callback: Optional[StatusCallback] = None,
|
||||
delay_seconds: int = 30,
|
||||
qr_group: str = DEFAULT_QR_GROUP,
|
||||
) -> dict:
|
||||
"""Send stored GroupHelp QR backup to every group owned by the userbot."""
|
||||
if not self.client:
|
||||
logger.error("Telegram client not initialized")
|
||||
return {
|
||||
"matched": 0,
|
||||
"sent": 0,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": ["Telegram client not initialized"],
|
||||
}
|
||||
|
||||
group = normalize_qr_group_name(qr_group)
|
||||
qr_data = get_qr_data(group)
|
||||
if not qr_data:
|
||||
await self._notify(status_callback, f"❌ No GroupHelp QR backup data configured for `{group}`. Use `/admin_set_qr {group} <qr_payload>`.")
|
||||
return {
|
||||
"matched": 0,
|
||||
"sent": 0,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": [f"No GroupHelp QR backup data configured for {group}"],
|
||||
}
|
||||
|
||||
sent = 0
|
||||
failed = 0
|
||||
skipped = 0
|
||||
errors = []
|
||||
owned_groups = []
|
||||
assignments = get_qr_group_assignments()
|
||||
|
||||
async for dialog in self.client.iter_dialogs():
|
||||
if not self._is_owned_group_dialog(dialog):
|
||||
continue
|
||||
assignment_group = self._dialog_assignment_group(dialog, assignments)
|
||||
if group == DEFAULT_QR_GROUP:
|
||||
if assignment_group not in (None, DEFAULT_QR_GROUP):
|
||||
skipped += 1
|
||||
continue
|
||||
elif assignment_group != group:
|
||||
skipped += 1
|
||||
continue
|
||||
owned_groups.append(dialog)
|
||||
|
||||
matched = len(owned_groups)
|
||||
if matched == 0:
|
||||
await self._notify(status_callback, f"⚠️ No owned groups found for GroupHelp QR sync group `{group}`.")
|
||||
return {
|
||||
"matched": matched,
|
||||
"sent": sent,
|
||||
"failed": failed,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
await self._notify(status_callback, f"🔄 Starting GroupHelp QR sync for `{group}` across {matched} owned groups...")
|
||||
|
||||
for index, dialog in enumerate(owned_groups):
|
||||
group_name = getattr(dialog, "name", None) or getattr(dialog.entity, "title", None) or str(dialog.id)
|
||||
|
||||
try:
|
||||
logger.info("Sending stored GroupHelp QR backup %s to owned group %s", group, group_name)
|
||||
qr_image = build_qr_image(qr_data)
|
||||
await self.client.send_file(
|
||||
dialog.entity,
|
||||
qr_image,
|
||||
caption=".importbackup",
|
||||
force_document=False,
|
||||
)
|
||||
sent += 1
|
||||
await self._notify(
|
||||
status_callback,
|
||||
f"✅ Sent GroupHelp QR `{group}` to {group_name} ({index + 1}/{matched})",
|
||||
)
|
||||
except Exception as e:
|
||||
failed += 1
|
||||
error = f"{group_name}: {e}"
|
||||
errors.append(error)
|
||||
logger.error("Failed to send GroupHelp QR backup %s to %s: %s", group, group_name, e)
|
||||
await self._notify(status_callback, f"⚠️ Failed to send GroupHelp QR `{group}` to {group_name}: {e}")
|
||||
|
||||
if index < matched - 1:
|
||||
await asyncio.sleep(delay_seconds)
|
||||
|
||||
await self._notify(
|
||||
status_callback,
|
||||
f"✅ GroupHelp QR sync for `{group}` complete. Sent: {sent}, failed: {failed}, owned groups: {matched}.",
|
||||
)
|
||||
|
||||
return {
|
||||
"matched": matched,
|
||||
"sent": sent,
|
||||
"failed": failed,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
async def sync_all_grouphelp_qr_groups(
|
||||
self,
|
||||
status_callback: Optional[StatusCallback] = None,
|
||||
delay_seconds: int = 30,
|
||||
) -> dict:
|
||||
"""Sync every configured logical GroupHelp QR group sequentially."""
|
||||
groups = list_qr_groups(include_assignments=True)
|
||||
if DEFAULT_QR_GROUP not in groups:
|
||||
groups.insert(0, DEFAULT_QR_GROUP)
|
||||
|
||||
totals = {
|
||||
"groups": [],
|
||||
"matched": 0,
|
||||
"sent": 0,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"errors": [],
|
||||
}
|
||||
|
||||
for index, group in enumerate(groups):
|
||||
result = await self.sync_grouphelp_qr_to_owned_groups(
|
||||
status_callback=status_callback,
|
||||
delay_seconds=delay_seconds,
|
||||
qr_group=group,
|
||||
)
|
||||
totals["groups"].append(group)
|
||||
totals["matched"] += result.get("matched", 0)
|
||||
totals["sent"] += result.get("sent", 0)
|
||||
totals["failed"] += result.get("failed", 0)
|
||||
totals["skipped"] += result.get("skipped", 0)
|
||||
totals["errors"].extend(result.get("errors", []))
|
||||
if index < len(groups) - 1:
|
||||
await asyncio.sleep(delay_seconds if result.get("matched", 0) > 0 else 1)
|
||||
|
||||
await self._notify(
|
||||
status_callback,
|
||||
f"✅ All GroupHelp QR syncs complete. Groups: {len(groups)}, sent: {totals['sent']}, failed: {totals['failed']}.",
|
||||
)
|
||||
return totals
|
||||
|
||||
async def add_users_to_group(self, group_id: str, user_ids: List[int]) -> bool:
|
||||
"""Add users to an existing Telegram group"""
|
||||
|
||||
Reference in New Issue
Block a user