Fix path
This commit is contained in:
@@ -19,9 +19,11 @@ The following commands can ONLY be run from the admin chat:
|
|||||||
#### User Management
|
#### User Management
|
||||||
```
|
```
|
||||||
/admin_add_user <username> - Add new user to database
|
/admin_add_user <username> - Add new user to database
|
||||||
|
/admin_add_user - Add a forwarded user to database
|
||||||
/admin_get_users - View default users list
|
/admin_get_users - View default users list
|
||||||
/admin_set_users <id_or_username> ... - Replace entire 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_add_users <id_or_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> - Remove users from default list
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -88,8 +88,10 @@ 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_add_user - Add a forwarded user to database
|
||||||
/admin_set_users <id_or_username> ... - Set default users for groups
|
/admin_set_users <id_or_username> ... - Set default users for groups
|
||||||
/admin_add_users <id_or_username> ... - Add users to default list
|
/admin_add_users <id_or_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> - Remove users from default list
|
||||||
/admin_get_users - Show current default users
|
/admin_get_users - Show current default users
|
||||||
/admin_set_qr <qr_payload> - Store GroupHelp backup payload rendered as a QR image
|
/admin_set_qr <qr_payload> - Store GroupHelp backup payload rendered as a QR image
|
||||||
|
|||||||
@@ -32,10 +32,34 @@ class AdminHandler:
|
|||||||
return user, True
|
return user, True
|
||||||
|
|
||||||
def _format_user(self, user: User, user_id: int, created: bool = False) -> str:
|
def _format_user(self, user: User, user_id: int, created: bool = False) -> str:
|
||||||
created_note = " (placeholder created)" if created else ""
|
notes = []
|
||||||
|
if created:
|
||||||
|
notes.append("placeholder created")
|
||||||
|
if not user.username and not user.access_hash:
|
||||||
|
notes.append("not inviteable yet")
|
||||||
|
created_note = f" ({', '.join(notes)})" if notes 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 _uninviteable_user_ids(self, user_ids: List[int]) -> List[int]:
|
||||||
|
missing = []
|
||||||
|
for user_id in user_ids:
|
||||||
|
user = self.user_service.get_user_by_id(user_id)
|
||||||
|
if user and not user.username and not user.access_hash:
|
||||||
|
missing.append(user_id)
|
||||||
|
return missing
|
||||||
|
|
||||||
|
def _append_uninviteable_warning(self, response: str, user_ids: List[int]) -> str:
|
||||||
|
missing = self._uninviteable_user_ids(user_ids)
|
||||||
|
if missing:
|
||||||
|
response += (
|
||||||
|
"\n\n⚠️ These numeric IDs are stored but not inviteable yet: "
|
||||||
|
f"{missing}. Re-add them with @username, or run `/admin_add_user` "
|
||||||
|
"or `/admin_add_users` with no args and forward a user message "
|
||||||
|
"so Telegram access_hash can be stored."
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
def _is_numeric_id(self, value) -> bool:
|
def _is_numeric_id(self, value) -> bool:
|
||||||
return str(value).strip().lstrip("-").isdigit()
|
return str(value).strip().lstrip("-").isdigit()
|
||||||
|
|
||||||
@@ -46,8 +70,16 @@ class AdminHandler:
|
|||||||
username=username.lstrip("@") if username else None,
|
username=username.lstrip("@") if username else None,
|
||||||
first_name=getattr(entity, "first_name", None) or getattr(entity, "title", None),
|
first_name=getattr(entity, "first_name", None) or getattr(entity, "title", None),
|
||||||
last_name=getattr(entity, "last_name", None),
|
last_name=getattr(entity, "last_name", None),
|
||||||
|
access_hash=getattr(entity, "access_hash", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _save_user_entity(self, entity, fallback_username: str = None) -> tuple:
|
||||||
|
user = self._user_from_entity(entity, fallback_username=fallback_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_identifier(self, identifier) -> tuple:
|
async def _resolve_user_identifier(self, identifier) -> tuple:
|
||||||
identifier = str(identifier).strip().rstrip(",")
|
identifier = str(identifier).strip().rstrip(",")
|
||||||
if not identifier:
|
if not identifier:
|
||||||
@@ -58,13 +90,9 @@ class AdminHandler:
|
|||||||
if self.telegram_client:
|
if self.telegram_client:
|
||||||
try:
|
try:
|
||||||
entity = await self.telegram_client.get_entity(user_id)
|
entity = await self.telegram_client.get_entity(user_id)
|
||||||
user = self._user_from_entity(entity)
|
return self._save_user_entity(entity)
|
||||||
existed = self.user_service.get_user_by_id(user.id) is not None
|
except Exception as e:
|
||||||
if not self.user_service.save_user(user):
|
logger.info("Could not resolve numeric user ID %s, creating placeholder: %s", user_id, e)
|
||||||
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)
|
user, created = self._ensure_user_record(user_id)
|
||||||
return user_id, user, created
|
return user_id, user, created
|
||||||
|
|
||||||
@@ -73,11 +101,7 @@ class AdminHandler:
|
|||||||
|
|
||||||
username = identifier.lstrip("@")
|
username = identifier.lstrip("@")
|
||||||
entity = await self.telegram_client.get_entity(username)
|
entity = await self.telegram_client.get_entity(username)
|
||||||
user = self._user_from_entity(entity, fallback_username=username)
|
return self._save_user_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:
|
async def _resolve_user_identifiers(self, identifiers: List) -> tuple:
|
||||||
resolved = []
|
resolved = []
|
||||||
@@ -151,6 +175,7 @@ class AdminHandler:
|
|||||||
response += "\n\nℹ️ New user records were created."
|
response += "\n\nℹ️ New user records were created."
|
||||||
if errors:
|
if errors:
|
||||||
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
|
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
|
||||||
|
response = self._append_uninviteable_warning(response, valid_user_ids)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
@@ -194,6 +219,7 @@ class AdminHandler:
|
|||||||
response += "\n\nℹ️ New user records were created."
|
response += "\n\nℹ️ New user records were created."
|
||||||
if errors:
|
if errors:
|
||||||
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
|
response += "\n\n⚠️ Some identifiers could not be resolved:\n" + "\n".join(errors)
|
||||||
|
response = self._append_uninviteable_warning(response, valid_user_ids)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
@@ -262,6 +288,34 @@ class AdminHandler:
|
|||||||
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)}"
|
||||||
|
|
||||||
|
async def handle_add_user_entity(self, chat_id: int, entity, add_to_defaults: bool = False) -> str:
|
||||||
|
"""Add a Telegram entity exposed by a forwarded user message."""
|
||||||
|
is_admin, error = self.verify_access(chat_id)
|
||||||
|
if not is_admin:
|
||||||
|
return error
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_id, user, created = self._save_user_entity(entity)
|
||||||
|
action = "added" if created else "updated"
|
||||||
|
user_label = f"@{user.username}" if user.username else user.name
|
||||||
|
response = f"✅ User {user_label} {action} successfully (ID: {user_id})."
|
||||||
|
|
||||||
|
if add_to_defaults:
|
||||||
|
current_users = get_default_group_users()
|
||||||
|
if user_id not in current_users:
|
||||||
|
if set_default_group_users(current_users + [user_id]):
|
||||||
|
response += "\n✅ User added to default group users."
|
||||||
|
else:
|
||||||
|
response += "\n❌ Failed to add user to default group users."
|
||||||
|
else:
|
||||||
|
response += "\nℹ️ User is already in default group users."
|
||||||
|
|
||||||
|
response = self._append_uninviteable_warning(response, [user_id])
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
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) -> str:
|
||||||
"""Retrieve current QR backup data (Admin only)"""
|
"""Retrieve current QR backup data (Admin only)"""
|
||||||
@@ -332,9 +386,11 @@ class AdminHandler:
|
|||||||
**Default Group Users Management:**
|
**Default Group Users Management:**
|
||||||
• `/admin_set_users <id_or_username> ...` - Replace entire 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_add_users <id_or_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> ...` - Remove users from default list
|
||||||
• `/admin_get_users` - Show current default users
|
• `/admin_get_users` - Show current default users
|
||||||
• `/admin_add_user <username>` - Resolve and save a user in the database
|
• `/admin_add_user <username>` - Resolve and save a user in the database
|
||||||
|
• `/admin_add_user` - Wait for a forwarded user message and save it
|
||||||
|
|
||||||
**QR Code Backup:**
|
**QR Code Backup:**
|
||||||
• `/admin_get_qr` - Get current QR backup data
|
• `/admin_get_qr` - Get current QR backup data
|
||||||
|
|||||||
87
src/main.py
87
src/main.py
@@ -1,7 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from telethon import TelegramClient, events
|
from telethon import TelegramClient, events, types
|
||||||
from src.config import load_config
|
from src.config import load_config
|
||||||
from src.services.mongodb_service import MongoDBService
|
from src.services.mongodb_service import MongoDBService
|
||||||
from src.services.user_service import UserService
|
from src.services.user_service import UserService
|
||||||
@@ -53,6 +53,7 @@ async def main():
|
|||||||
admin_handler.set_client(client)
|
admin_handler.set_client(client)
|
||||||
group_conversations = {}
|
group_conversations = {}
|
||||||
qr_import_sessions = {}
|
qr_import_sessions = {}
|
||||||
|
user_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(
|
||||||
@@ -92,6 +93,27 @@ async def main():
|
|||||||
return "❌ Could not download the forwarded QR image."
|
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)
|
||||||
|
|
||||||
|
async def handle_forwarded_user_import(event, add_to_defaults: bool) -> str:
|
||||||
|
message = event.message
|
||||||
|
forward_info = getattr(message, "fwd_from", None)
|
||||||
|
if not forward_info:
|
||||||
|
return "❌ Please forward a message from the Telegram user. Do not paste a numeric ID."
|
||||||
|
|
||||||
|
from_id = getattr(forward_info, "from_id", None)
|
||||||
|
if not isinstance(from_id, types.PeerUser):
|
||||||
|
return "❌ Forwarded message must come from a Telegram user, not a channel/group or hidden sender."
|
||||||
|
|
||||||
|
try:
|
||||||
|
entity = await client.get_entity(from_id)
|
||||||
|
except Exception as e:
|
||||||
|
return f"❌ Could not resolve forwarded user entity: {e}"
|
||||||
|
|
||||||
|
return await admin_handler.handle_add_user_entity(
|
||||||
|
event.chat_id,
|
||||||
|
entity,
|
||||||
|
add_to_defaults=add_to_defaults,
|
||||||
|
)
|
||||||
|
|
||||||
@client.on(events.NewMessage())
|
@client.on(events.NewMessage())
|
||||||
async def message_handler(event):
|
async def message_handler(event):
|
||||||
@@ -134,6 +156,9 @@ async def main():
|
|||||||
if sender_id in qr_import_sessions:
|
if sender_id in qr_import_sessions:
|
||||||
del qr_import_sessions[sender_id]
|
del qr_import_sessions[sender_id]
|
||||||
canceled = True
|
canceled = True
|
||||||
|
if sender_id in user_import_sessions:
|
||||||
|
del user_import_sessions[sender_id]
|
||||||
|
canceled = True
|
||||||
|
|
||||||
if canceled:
|
if canceled:
|
||||||
await event.respond('🛑 Operation canceled.')
|
await event.respond('🛑 Operation canceled.')
|
||||||
@@ -141,7 +166,11 @@ async def main():
|
|||||||
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'):
|
if (
|
||||||
|
sender_id in qr_import_sessions
|
||||||
|
and not lower_text.startswith('/admin_set_qr')
|
||||||
|
and (message.fwd_from or not raw_text.startswith('/'))
|
||||||
|
):
|
||||||
session = qr_import_sessions[sender_id]
|
session = qr_import_sessions[sender_id]
|
||||||
if session["chat_id"] != chat_id:
|
if session["chat_id"] != chat_id:
|
||||||
return
|
return
|
||||||
@@ -152,6 +181,23 @@ async def main():
|
|||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
sender_id in user_import_sessions
|
||||||
|
and (message.fwd_from or not raw_text.startswith('/'))
|
||||||
|
):
|
||||||
|
session = user_import_sessions[sender_id]
|
||||||
|
if session["chat_id"] != chat_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
response = await handle_forwarded_user_import(
|
||||||
|
event,
|
||||||
|
add_to_defaults=session["add_to_defaults"],
|
||||||
|
)
|
||||||
|
if response.startswith("✅"):
|
||||||
|
user_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()
|
||||||
@@ -242,9 +288,23 @@ async def main():
|
|||||||
parts = text.split()
|
parts = text.split()
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
response = await admin_handler.handle_add_to_default_users(chat_id, parts[1:])
|
response = await admin_handler.handle_add_to_default_users(chat_id, parts[1:])
|
||||||
|
user_import_sessions.pop(sender_id, None)
|
||||||
|
qr_import_sessions.pop(sender_id, None)
|
||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
else:
|
else:
|
||||||
await event.respond("❌ Please provide at least one user ID or username.\n\nUsage: `/admin_add_users <id_or_username1> <id_or_username2> ...`")
|
is_admin, error = admin_handler.verify_access(chat_id)
|
||||||
|
if not is_admin:
|
||||||
|
await event.respond(error)
|
||||||
|
else:
|
||||||
|
user_import_sessions[sender_id] = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"add_to_defaults": True,
|
||||||
|
}
|
||||||
|
qr_import_sessions.pop(sender_id, None)
|
||||||
|
await event.respond(
|
||||||
|
"👤 Forward a message from the Telegram user to add them to the database and default group users.\n\n"
|
||||||
|
"The forward must expose the sender. Send `!cancel` to abort."
|
||||||
|
)
|
||||||
|
|
||||||
elif text.startswith('/admin_remove_users'):
|
elif text.startswith('/admin_remove_users'):
|
||||||
parts = text.split()
|
parts = text.split()
|
||||||
@@ -255,14 +315,27 @@ async def main():
|
|||||||
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 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
|
|
||||||
parts = text.split(maxsplit=1)
|
parts = text.split(maxsplit=1)
|
||||||
if len(parts) > 1:
|
if len(parts) > 1:
|
||||||
username = parts[1].strip()
|
username = parts[1].strip()
|
||||||
response = await admin_handler.handle_add_user_to_db(chat_id, username)
|
response = await admin_handler.handle_add_user_to_db(chat_id, username)
|
||||||
|
user_import_sessions.pop(sender_id, None)
|
||||||
|
qr_import_sessions.pop(sender_id, None)
|
||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
else:
|
else:
|
||||||
await event.respond("❌ Please provide a username.\n\nUsage: `/admin_add_user <username>`")
|
is_admin, error = admin_handler.verify_access(chat_id)
|
||||||
|
if not is_admin:
|
||||||
|
await event.respond(error)
|
||||||
|
else:
|
||||||
|
user_import_sessions[sender_id] = {
|
||||||
|
"chat_id": chat_id,
|
||||||
|
"add_to_defaults": False,
|
||||||
|
}
|
||||||
|
qr_import_sessions.pop(sender_id, None)
|
||||||
|
await event.respond(
|
||||||
|
"👤 Forward a message from the Telegram user to add them to the database.\n\n"
|
||||||
|
"The forward must expose the sender. Send `!cancel` to abort."
|
||||||
|
)
|
||||||
|
|
||||||
elif text.startswith('/admin_get_qr'):
|
elif text.startswith('/admin_get_qr'):
|
||||||
response = await admin_handler.handle_get_qr_backup(chat_id)
|
response = await admin_handler.handle_get_qr_backup(chat_id)
|
||||||
@@ -274,6 +347,7 @@ async def main():
|
|||||||
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)
|
qr_import_sessions.pop(sender_id, None)
|
||||||
|
user_import_sessions.pop(sender_id, None)
|
||||||
await event.respond(response)
|
await event.respond(response)
|
||||||
else:
|
else:
|
||||||
is_admin, error = admin_handler.verify_access(chat_id)
|
is_admin, error = admin_handler.verify_access(chat_id)
|
||||||
@@ -281,6 +355,7 @@ async def main():
|
|||||||
await event.respond(error)
|
await event.respond(error)
|
||||||
else:
|
else:
|
||||||
qr_import_sessions[sender_id] = {"chat_id": chat_id}
|
qr_import_sessions[sender_id] = {"chat_id": chat_id}
|
||||||
|
user_import_sessions.pop(sender_id, None)
|
||||||
await event.respond(
|
await event.respond(
|
||||||
"📷 Forward the original GroupHelp QR image message here.\n\n"
|
"📷 Forward the original GroupHelp QR image message here.\n\n"
|
||||||
"It must be a forwarded image message with `.importbackup` as its caption/body. "
|
"It must be a forwarded image message with `.importbackup` as its caption/body. "
|
||||||
@@ -383,8 +458,10 @@ async def main():
|
|||||||
• `/admin_get_users` - Show default group users
|
• `/admin_get_users` - Show default group users
|
||||||
• `/admin_set_users <id_or_username> ...` - Set default users
|
• `/admin_set_users <id_or_username> ...` - Set default users
|
||||||
• `/admin_add_users <id_or_username> ...` - Add users to default list
|
• `/admin_add_users <id_or_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> ...` - Remove users from default list
|
||||||
• `/admin_add_user <username>` - Add new user to database
|
• `/admin_add_user <username>` - Add new user to database
|
||||||
|
• `/admin_add_user` - Add a forwarded user to database
|
||||||
• `/admin_get_qr` - Get QR backup data
|
• `/admin_get_qr` - Get QR backup data
|
||||||
• `/admin_set_qr <qr_payload>` - Set QR backup data directly
|
• `/admin_set_qr <qr_payload>` - Set QR backup data directly
|
||||||
• `/admin_set_qr` - Decode a forwarded `.importbackup` QR image
|
• `/admin_set_qr` - Decode a forwarded `.importbackup` QR image
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class User:
|
|||||||
username: Optional[str] = None
|
username: Optional[str] = None
|
||||||
first_name: Optional[str] = None
|
first_name: Optional[str] = None
|
||||||
last_name: Optional[str] = None
|
last_name: Optional[str] = None
|
||||||
|
access_hash: Optional[int] = None
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.id is None:
|
if self.id is None:
|
||||||
@@ -25,7 +26,8 @@ class User:
|
|||||||
id=data.get('id'),
|
id=data.get('id'),
|
||||||
username=data.get('username'),
|
username=data.get('username'),
|
||||||
first_name=data.get('first_name'),
|
first_name=data.get('first_name'),
|
||||||
last_name=data.get('last_name')
|
last_name=data.get('last_name'),
|
||||||
|
access_hash=data.get('access_hash')
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
@@ -34,5 +36,6 @@ class User:
|
|||||||
'id': self.id,
|
'id': self.id,
|
||||||
'username': self.username,
|
'username': self.username,
|
||||||
'first_name': self.first_name,
|
'first_name': self.first_name,
|
||||||
'last_name': self.last_name
|
'last_name': self.last_name,
|
||||||
|
'access_hash': self.access_hash
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,49 @@ 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 _user_label(self, user) -> str:
|
||||||
|
if user.username:
|
||||||
|
return f"@{user.username}"
|
||||||
|
return str(user.id)
|
||||||
|
|
||||||
|
def _save_resolved_access_hash(self, user, input_user) -> None:
|
||||||
|
access_hash = getattr(input_user, "access_hash", None)
|
||||||
|
user_id = getattr(input_user, "user_id", None) or getattr(input_user, "id", None)
|
||||||
|
if access_hash and user_id:
|
||||||
|
user.id = user_id
|
||||||
|
user.access_hash = access_hash
|
||||||
|
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)
|
||||||
|
references.append(user.id)
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for reference in references:
|
||||||
|
try:
|
||||||
|
input_user = await self.client.get_input_entity(reference)
|
||||||
|
self._save_resolved_access_hash(user, input_user)
|
||||||
|
access_hash = getattr(input_user, "access_hash", None)
|
||||||
|
user_id = getattr(input_user, "user_id", None) or getattr(input_user, "id", None)
|
||||||
|
if access_hash and user_id:
|
||||||
|
return types.InputUser(user_id, access_hash)
|
||||||
|
return input_user
|
||||||
|
except ValueError as e:
|
||||||
|
last_error = e
|
||||||
|
|
||||||
|
if not user.username:
|
||||||
|
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 "
|
||||||
|
"forward a user message."
|
||||||
|
) from last_error
|
||||||
|
raise last_error or ValueError("could not resolve Telegram user")
|
||||||
|
|
||||||
async def create_group(
|
async def create_group(
|
||||||
self,
|
self,
|
||||||
group_name: str,
|
group_name: str,
|
||||||
@@ -147,8 +190,8 @@ class GroupService:
|
|||||||
f"👥 Adding users: {index}/{len(users)} completed",
|
f"👥 Adding users: {index}/{len(users)} completed",
|
||||||
)
|
)
|
||||||
|
|
||||||
user_ref = user.username or user.id
|
user_ref = self._user_label(user)
|
||||||
user_to_add = await self.client.get_input_entity(user_ref)
|
user_to_add = await self._resolve_input_user(user)
|
||||||
await self.client(InviteToChannelRequest(target_group, [user_to_add]))
|
await self.client(InviteToChannelRequest(target_group, [user_to_add]))
|
||||||
await self._promote_user(target_group, user_to_add, full_admin=False)
|
await self._promote_user(target_group, user_to_add, full_admin=False)
|
||||||
success_count += 1
|
success_count += 1
|
||||||
@@ -159,18 +202,17 @@ class GroupService:
|
|||||||
await self._notify(status_callback, "⚠️ Telegram flood limit reached. Pausing for 30 seconds...")
|
await self._notify(status_callback, "⚠️ Telegram flood limit reached. Pausing for 30 seconds...")
|
||||||
await asyncio.sleep(30)
|
await asyncio.sleep(30)
|
||||||
except UserPrivacyRestrictedError:
|
except UserPrivacyRestrictedError:
|
||||||
logger.warning(f"User {user.username or user.id} has privacy restrictions")
|
logger.warning(f"User {self._user_label(user)} has privacy restrictions")
|
||||||
error_count += 1
|
error_count += 1
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Could not resolve Telegram entity for user {user.username or user.id}: {e}")
|
logger.error(f"Could not resolve Telegram entity for user {self._user_label(user)}: {e}")
|
||||||
await self._notify(
|
await self._notify(
|
||||||
status_callback,
|
status_callback,
|
||||||
f"⚠️ Could not resolve user {user.username or user.id}. "
|
f"⚠️ Could not resolve user {self._user_label(user)}: {e}"
|
||||||
"If this is a numeric ID, the userbot must have seen that user or you must store a username."
|
|
||||||
)
|
)
|
||||||
error_count += 1
|
error_count += 1
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding user {user.username or user.id}: {e}")
|
logger.error(f"Error adding user {self._user_label(user)}: {e}")
|
||||||
error_count += 1
|
error_count += 1
|
||||||
if error_count > 10:
|
if error_count > 10:
|
||||||
await self._notify(status_callback, "❌ Too many errors, aborting user addition!")
|
await self._notify(status_callback, "❌ Too many errors, aborting user addition!")
|
||||||
@@ -272,9 +314,9 @@ class GroupService:
|
|||||||
|
|
||||||
group = await self.client.get_input_entity(group_id)
|
group = await self.client.get_input_entity(group_id)
|
||||||
for user in users:
|
for user in users:
|
||||||
user_ref = user.username or user.id
|
user_ref = self._user_label(user)
|
||||||
try:
|
try:
|
||||||
user_to_add = await self.client.get_input_entity(user_ref)
|
user_to_add = await self._resolve_input_user(user)
|
||||||
await self.client(InviteToChannelRequest(group, [user_to_add]))
|
await self.client(InviteToChannelRequest(group, [user_to_add]))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"Could not resolve Telegram entity for user {user_ref}: {e}")
|
logger.error(f"Could not resolve Telegram entity for user {user_ref}: {e}")
|
||||||
|
|||||||
Reference in New Issue
Block a user