This commit is contained in:
Corrado Mulas
2026-06-19 15:11:04 +02:00
parent 0d26d5a8d3
commit 512e64f83d
10 changed files with 407 additions and 145 deletions

View File

@@ -75,6 +75,15 @@ docker run telegram-groupfactory
## Quick Start
### GroupFactory Commands (Staff Chat Only)
```
!newgrp - Start interactive group creation
!confirm - Confirm pending group creation
!cancel - Cancel pending group creation
!PING - Check if the userbot is online
!help - Show GroupFactory commands
```
### Admin Commands (Admin Chat Only)
```
/admin_add_user <username> - Add user to database
@@ -86,9 +95,9 @@ docker run telegram-groupfactory
/admin_get_qr - Retrieve GroupHelp backup QR data
```
### User Commands
### Legacy User Commands
```
/create_group <name> - Create group with default users
/create_group <name> - Deprecated; use !newgrp in staff chat
/users - List all users
/user <user_id> - Get user info
/help - Show all available commands
@@ -96,7 +105,8 @@ docker run telegram-groupfactory
## Available Commands
- `/create_group <name>` - Create a new group
- `!newgrp` - Create a new group with DB users and stored GroupHelp QR import
- `/create_group <name>` - Deprecated; use `!newgrp`
- `/add_users <group_id> <user_ids>` - Add users to a group
- `/get_group <group_id>` - Get group information
- `/users` - List all users

View File

@@ -6,7 +6,6 @@ from src.api.schemas import (
CommandResponse,
CreateGroupRequest,
)
from src.config import save_user_admin_role
def _envelope(message: str) -> CommandResponse:
@@ -19,14 +18,13 @@ router = APIRouter(prefix="/api/groups", tags=["groups"], dependencies=[Depends(
@router.post("", response_model=CommandResponse)
async def create_group(payload: CreateGroupRequest, request: Request):
handler = request.app.state.group_handler
message = await handler.handle_create_group(payload.name, payload.user_ids)
# Mirror the Telegram inline-button flow: persist the admin-role choice
# against the bot user (the entity creating the group via the API).
if payload.full_admin is not None:
bot_user_id = request.app.state.config["telegram"].get("factory_bot_id")
if bot_user_id:
save_user_admin_role(bot_user_id, payload.full_admin)
message = await handler.handle_create_group(
payload.name,
user_ids=payload.user_ids,
description=payload.description or payload.name,
staff_chat_id=request.app.state.config["telegram"].get("staff_chat_id"),
factory_bot_id=request.app.state.config["telegram"].get("factory_bot_id"),
)
return _envelope(message)

View File

@@ -15,6 +15,7 @@ class AddUserRequest(BaseModel):
class CreateGroupRequest(BaseModel):
name: str = Field(..., min_length=1)
description: Optional[str] = None
user_ids: Optional[List[int]] = None
full_admin: Optional[bool] = None

View File

@@ -2,7 +2,7 @@ import logging
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, save_user_admin_role
set_qr_backup_data, verify_admin_access
)
from src.services.user_service import UserService
from src.models.user import User
@@ -191,7 +191,7 @@ class AdminHandler:
import hashlib
user_id = int(hashlib.md5(username.encode()).hexdigest()[:8], 16)
user = User(id=user_id, username=username, name=username)
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})"

View File

@@ -1,10 +1,12 @@
import logging
from typing import List
from typing import Awaitable, Callable, List, Optional
from src.services.user_service import UserService
from src.services.group_service import GroupService
logger = logging.getLogger(__name__)
StatusCallback = Callable[[str], Awaitable[object]]
class GroupHandler:
"""Handler class for group-related commands"""
@@ -12,12 +14,34 @@ class GroupHandler:
self.user_service = user_service
self.group_service = group_service
async def handle_create_group(self, group_name: str, user_ids: List[int]) -> str:
async def handle_create_group(
self,
group_name: str,
user_ids: List[int] = None,
description: str = "",
status_callback: Optional[StatusCallback] = None,
staff_chat_id: Optional[int] = None,
factory_bot_id: Optional[int] = None,
) -> str:
"""Handle command to create a new group"""
try:
group_id = await self.group_service.create_group(group_name, user_ids)
if group_id:
return f"✅ Group '{group_name}' created successfully with ID: {group_id}"
result = await self.group_service.create_group(
group_name,
description=description,
user_ids=user_ids,
status_callback=status_callback,
staff_chat_id=staff_chat_id,
factory_bot_id=factory_bot_id,
)
if result:
invite = f"\n🔗 {result['invite_link']}" if result.get("invite_link") else ""
qr_status = "\n✅ GroupHelp QR import sent" if result.get("qr_imported") else "\n⚠️ GroupHelp QR import not configured"
return (
f"✅ Group '{group_name}' created successfully with ID: {result['id']}\n"
f"👥 Added {result['users_added']}/{result['users_total']} users"
f"{qr_status}"
f"{invite}"
)
else:
return "❌ Failed to create group"
except Exception as e:

View File

@@ -43,7 +43,7 @@ class UserHandler:
import hashlib
user_id = int(hashlib.md5(username.encode()).hexdigest()[:8], 16)
user = User(id=user_id, username=username, name=username)
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})"

View File

@@ -1,9 +1,8 @@
import asyncio
import logging
import json
import uvicorn
from telethon import Button, TelegramClient, events
from src.config import load_config, save_user_admin_role, get_user_admin_role
from telethon import TelegramClient, events
from src.config import load_config
from src.services.mongodb_service import MongoDBService
from src.services.user_service import UserService
from src.services.group_service import GroupService
@@ -25,6 +24,8 @@ async def main():
# Load configuration
config = load_config()
staff_chat_id = config['telegram']['staff_chat_id']
factory_bot_id = config['telegram']['factory_bot_id']
# Initialize MongoDB service
mongo_service = MongoDBService(
@@ -47,46 +48,21 @@ async def main():
config['telegram']['api_id'],
config['telegram']['api_hash']
)
group_service.set_client(client)
group_conversations = {}
@client.on(events.CallbackQuery())
async def callback_handler(event):
"""Handle inline button callbacks for admin role selection"""
try:
data = event.data.decode() if isinstance(event.data, bytes) else event.data
async def create_group_from_staff_flow(event, name: str, description: str):
response = await group_handler.handle_create_group(
name,
description=description,
status_callback=event.respond,
staff_chat_id=staff_chat_id,
factory_bot_id=factory_bot_id,
)
await event.respond(response)
# Parse callback data
if data.startswith('admin_role:'):
user_id = event.sender_id
is_full_admin = data.split(':')[1] == 'yes'
# Save the user's admin role preference
if save_user_admin_role(user_id, is_full_admin):
role_text = "✅ Full Group Admin" if is_full_admin else "👤 Regular Member"
await event.answer(f"Set as {role_text}")
# Edit the message to show the selection
await event.edit(f"Group Admin Role Selection\n\n{role_text} - Confirmed!")
else:
await event.answer("❌ Failed to save preference", alert=True)
elif data.startswith('group_create:'):
# Extract group name and user IDs from callback
parts = data.split(':', 2)
if len(parts) >= 3:
group_name = parts[1]
user_ids_str = parts[2]
try:
user_ids = [int(uid) for uid in user_ids_str.split(',')] if user_ids_str else None
response = await group_handler.handle_create_group(group_name, user_ids)
await event.answer()
await event.edit(response)
except ValueError:
await event.answer("❌ Invalid user IDs", alert=True)
except Exception as e:
logger.error(f"Error handling callback: {e}")
await event.answer(f"❌ Error: {str(e)}", alert=True)
def is_staff_chat(chat_id: int) -> bool:
return chat_id == staff_chat_id
@client.on(events.NewMessage())
async def message_handler(event):
@@ -101,6 +77,105 @@ async def main():
return
try:
# ==================== IMS GROUPFACTORY STAFF FLOW ====================
if is_staff_chat(chat_id):
raw_text = text.strip()
lower_text = raw_text.lower()
if lower_text == '!ping':
await event.respond('PONG -> IMS GroupFactory Userbot')
return
if lower_text == '!help':
help_text = """**IMS GroupFactory Commands**
`!newgrp` - Start creating a new Telegram group
`!cancel` - Cancel the current operation
`!confirm` - Confirm group creation
`!PING` - Check if the userbot is online
`!help` - Display this help message"""
await event.respond(help_text)
return
if lower_text == '!cancel':
if sender_id in group_conversations:
del group_conversations[sender_id]
await event.respond('🛑 Operation canceled.')
else:
await event.respond('❓ No active operation to cancel.')
return
if lower_text.startswith('!newgrp'):
if lower_text != '!newgrp':
cmd_args = raw_text[len('!newgrp'):].strip()
marker = "\n\nDescription:\n"
if marker in cmd_args:
name, description = cmd_args.split(marker, 1)
name = name.strip()
description = description.strip()
if not name or not description:
await event.respond('⚠️ Group name and description cannot be empty.')
return
await create_group_from_staff_flow(event, name, description)
else:
await event.respond('⚠️ Old format detected but missing description.\nUse `!newgrp` alone to start interactive mode.')
return
group_conversations[sender_id] = {
'step': 'name',
'data': {}
}
await event.respond('👋 Let\'s create a new group!\n\nPlease enter the group name:')
return
if sender_id in group_conversations:
current_step = group_conversations[sender_id]['step']
if lower_text == '!confirm' and current_step == 'confirm':
name = group_conversations[sender_id]['data']['name']
description = group_conversations[sender_id]['data']['description']
await event.respond('✅ Confirmed! Starting group creation process...')
try:
await create_group_from_staff_flow(event, name, description)
finally:
group_conversations.pop(sender_id, None)
return
if raw_text.startswith('!'):
if current_step == 'confirm':
await event.respond('❓ Please type `!confirm` to create the group or `!cancel` to abort.')
return
if current_step == 'name':
if not raw_text:
await event.respond('⚠️ Group name cannot be empty. Please try again:')
return
group_conversations[sender_id]['data']['name'] = raw_text
group_conversations[sender_id]['step'] = 'description'
await event.respond(f'📝 Group name set to: "{raw_text}"\n\nNow please enter the group description:')
return
if current_step == 'description':
if not raw_text:
await event.respond('⚠️ Group description cannot be empty. Please try again:')
return
group_conversations[sender_id]['data']['description'] = raw_text
name = group_conversations[sender_id]['data']['name']
group_conversations[sender_id]['step'] = 'confirm'
await event.respond(
f'📋 **Group Creation Summary**\n\n'
f'**Name**: {name}\n'
f'**Description**: {raw_text}\n\n'
f'Type `!confirm` to create this group or `!cancel` to abort.'
)
return
if current_step == 'confirm':
await event.respond('❓ Please type `!confirm` to create the group or `!cancel` to abort.')
return
# ==================== ADMIN COMMANDS ====================
# All admin commands require being in the admin chat
@@ -178,36 +253,7 @@ async def main():
# ==================== GROUP COMMANDS ====================
elif text.startswith('/create_group'):
# Parse group name and optional user IDs
parts = text.split(maxsplit=2)
if len(parts) > 1:
group_name = parts[1]
user_ids = None
if len(parts) > 2:
try:
user_ids = [int(uid) for uid in parts[2].split(',')]
except ValueError:
await event.respond("❌ Invalid user IDs format.\n\nUsage: `/create_group <name>` or `/create_group <name> <user_id1>,<user_id2>,...`")
return
# Create the group
response = await group_handler.handle_create_group(group_name, user_ids)
# Ask about admin role with inline buttons
buttons = [
[
Button.inline("✅ Yes, I want to be full admin", b"admin_role:yes"),
Button.inline("❌ No, just regular member", b"admin_role:no"),
]
]
await event.respond(
f"{response}\n\n" +
"👤 Would you like to be added as a full admin to this group?",
buttons=buttons
)
else:
await event.respond("❌ Please provide a group name.\n\nUsage: `/create_group <name>` or `/create_group <name> <user_id1>,<user_id2>,...`")
await event.respond(" `/create_group` has been replaced. Use `!newgrp` in the staff chat.")
elif text.startswith('/add_users'):
# Parse group ID and user IDs
@@ -287,8 +333,8 @@ async def main():
• `/delete_user <user_id>` - Delete user
**Group Management:**
• `/create_group <name>` - Create group with default users
• `/create_group <name> <id1>,<id2>` - Create group with specific users
• `!newgrp` - Create a group with database users and stored GroupHelp QR import
• `/create_group` - Deprecated; use `!newgrp`
• `/add_users <group_id> <id1>,<id2>` - Add users to group
• `/get_group <group_id>` - Get group info

View File

@@ -13,6 +13,11 @@ class User:
if self.id is None:
raise ValueError("User ID cannot be None")
@property
def name(self) -> str:
parts = [part for part in [self.first_name, self.last_name] if part]
return " ".join(parts) or self.username or str(self.id)
@classmethod
def from_dict(cls, data: dict):
"""Create User instance from dictionary"""

View File

@@ -1,32 +1,135 @@
import logging
from typing import List, Optional
from src.models.user import User
import asyncio
from typing import Awaitable, Callable, List, Optional
from telethon import functions, types
from telethon.errors.rpcerrorlist import PeerFloodError, UserPrivacyRestrictedError
from telethon.tl.functions.channels import CreateChannelRequest, InviteToChannelRequest
from src.services.mongodb_service import MongoDBService
from src.config import get_telegram_client, get_default_group_users
from src.config import get_default_group_users, get_qr_data
logger = logging.getLogger(__name__)
StatusCallback = Callable[[str], Awaitable[object]]
class GroupService:
"""Service class for group creation and management"""
def __init__(self, mongo_service: MongoDBService):
def __init__(self, mongo_service: MongoDBService, telegram_client=None):
self.mongo_service = mongo_service
self.client = get_telegram_client()
self.client = telegram_client
async def create_group(self, group_name: str, user_ids: List[int] = None) -> Optional[str]:
"""Create a Telegram group with specified users or default users"""
def set_client(self, telegram_client):
self.client = telegram_client
async def _notify(self, status_callback: Optional[StatusCallback], message: str):
if status_callback:
return await status_callback(message)
return None
async def _edit_status(self, status_message, message: str):
if hasattr(status_message, "edit"):
await status_message.edit(message)
else:
await self.client.edit_message(status_message, message)
def _admin_rights(self, **rights):
try:
return types.ChatAdminRights(**rights)
except TypeError:
rights.pop("manage_topics", None)
return types.ChatAdminRights(**rights)
async def _promote_user(self, channel, user_id, full_admin: bool):
if full_admin:
admin_rights = self._admin_rights(
change_info=True,
post_messages=True,
edit_messages=True,
delete_messages=True,
ban_users=True,
invite_users=True,
pin_messages=True,
add_admins=True,
anonymous=False,
manage_call=True,
other=True,
manage_topics=True,
)
rank = "manager"
else:
admin_rights = self._admin_rights(
change_info=False,
post_messages=True,
edit_messages=True,
delete_messages=True,
ban_users=True,
invite_users=True,
pin_messages=True,
add_admins=False,
anonymous=False,
manage_call=True,
other=False,
manage_topics=True,
)
rank = "admin"
await self.client(functions.channels.EditAdminRequest(
channel=channel,
user_id=user_id,
admin_rights=admin_rights,
rank=rank,
))
async def _export_invite_link(self, channel) -> Optional[str]:
try:
invite = await self.client(functions.messages.ExportChatInviteRequest(
peer=channel,
))
return getattr(invite, "link", None)
except Exception as e:
logger.warning(f"Failed to export invite link: {e}")
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(
self,
group_name: str,
description: str = "",
user_ids: List[int] = None,
status_callback: Optional[StatusCallback] = None,
staff_chat_id: Optional[int] = None,
factory_bot_id: Optional[int] = None,
) -> Optional[dict]:
"""Create a Telegram megagroup and run the IMS GroupFactory setup flow."""
try:
if not self.client:
logger.error("Telegram client not initialized")
return None
# Use default users if none specified
await self._notify(status_callback, f'🚀 Creating group "{group_name}"...')
created_channel = await self.client(CreateChannelRequest(
group_name,
description,
megagroup=True,
broadcast=False,
))
channel = created_channel.chats[0]
target_group = types.InputPeerChannel(channel.id, channel.access_hash)
await self._notify(status_callback, f'✅ Group "{group_name}" created successfully!')
if user_ids is None or len(user_ids) == 0:
user_ids = get_default_group_users()
if len(user_ids) == 0:
logger.warning("No default users configured for group creation")
# Get users from MongoDB
users = []
for user_id in user_ids:
user = self.mongo_service.get_user_by_id(user_id)
@@ -35,19 +138,98 @@ class GroupService:
else:
logger.warning(f"User {user_id} not found in database")
if not users:
logger.warning("No valid users found for group creation")
await self._notify(status_callback, f"👥 Found {len(users)} configured users to add")
status_message = await self._notify(status_callback, "👥 Adding users to the group...")
success_count = 0
error_count = 0
for index, user in enumerate(users):
try:
if index % 5 == 0 and status_message:
await self._edit_status(
status_message,
f"👥 Adding users: {index}/{len(users)} completed",
)
user_ref = user.username or user.id
user_to_add = await self.client.get_input_entity(user_ref)
await self.client(InviteToChannelRequest(target_group, [user_to_add]))
await self._promote_user(target_group, user_to_add, full_admin=False)
success_count += 1
await asyncio.sleep(2)
except PeerFloodError:
logger.warning("Telegram flood limit reached while adding users")
await self._notify(status_callback, "⚠️ Telegram flood limit reached. Pausing for 30 seconds...")
await asyncio.sleep(30)
except UserPrivacyRestrictedError:
logger.warning(f"User {user.username or user.id} has privacy restrictions")
error_count += 1
except Exception as e:
logger.error(f"Error adding user {user.username or user.id}: {e}")
error_count += 1
if error_count > 10:
await self._notify(status_callback, "❌ Too many errors, aborting user addition!")
return None
# Create group using Telegram API
result = await self.client.create_group(group_name, [user.id for user in users])
if status_message:
await self._edit_status(
status_message,
f"✅ Added {success_count}/{len(users)} users to the group",
)
if result:
logger.info(f"Successfully created group '{group_name}' with {len(users)} members")
return result.id
if factory_bot_id:
try:
logger.info(f"Promoting factory bot {factory_bot_id} as manager")
factory_bot = await self.client.get_input_entity(factory_bot_id)
try:
await self.client(InviteToChannelRequest(target_group, [factory_bot]))
except Exception as e:
logger.info(f"Factory bot invite skipped or failed before promotion: {e}")
await self._promote_user(target_group, factory_bot, full_admin=True)
await asyncio.sleep(2)
except Exception as e:
logger.error(f"Failed to promote factory bot {factory_bot_id}: {e}")
await self._notify(status_callback, f"⚠️ Failed to promote factory bot: {e}")
await self._notify(status_callback, "⚙️ Sending GroupHelp setup commands...")
await self.client.send_message(target_group, "/pro")
await asyncio.sleep(2)
if staff_chat_id:
await self.client.send_message(target_group, f"/setstaffgroup {staff_chat_id}")
await asyncio.sleep(2)
qr_imported = False
qr_data = get_qr_data()
if qr_data:
logger.info("Sending GroupHelp QR backup import command")
await self.client.send_message(
target_group,
self._build_importbackup_message(qr_data),
)
qr_imported = True
if staff_chat_id:
await self.client.send_message(staff_chat_id, "✅ GroupHelp QR backup import sent to the new group.")
elif staff_chat_id:
await self.client.send_message(staff_chat_id, "⚠️ No GroupHelp QR backup data configured. Use `/admin_set_qr <qr_data>`.")
invite_link = await self._export_invite_link(target_group)
if staff_chat_id:
if invite_link:
await self.client.send_message(staff_chat_id, f"🔗 Invite link:\n\n{invite_link}")
else:
logger.error("Failed to create group")
return None
await self.client.send_message(staff_chat_id, "⚠️ Failed to generate invite link")
logger.info(f"Group creation process completed for {group_name}")
return {
"id": channel.id,
"access_hash": channel.access_hash,
"title": channel.title,
"invite_link": invite_link,
"users_added": success_count,
"users_total": len(users),
"qr_imported": qr_imported,
}
except Exception as e:
logger.error(f"Error creating group '{group_name}': {e}")
@@ -60,7 +242,6 @@ class GroupService:
logger.error("Telegram client not initialized")
return False
# Get users from MongoDB
users = []
for user_id in user_ids:
user = self.mongo_service.get_user_by_id(user_id)
@@ -73,15 +254,14 @@ class GroupService:
logger.warning("No valid users found for adding to group")
return False
# Add users to group using Telegram API
result = await self.client.add_users_to_group(group_id, [user.id for user in users])
group = await self.client.get_input_entity(group_id)
for user in users:
user_ref = user.username or user.id
user_to_add = await self.client.get_input_entity(user_ref)
await self.client(InviteToChannelRequest(group, [user_to_add]))
if result:
logger.info(f"Successfully added {len(users)} users to group {group_id}")
return True
else:
logger.error("Failed to add users to group")
return False
except Exception as e:
logger.error(f"Error adding users to group {group_id}: {e}")
@@ -94,15 +274,13 @@ class GroupService:
logger.error("Telegram client not initialized")
return None
# Get group info using Telegram API
result = await self.client.get_group_info(group_id)
if result:
result = await self.client.get_entity(group_id)
logger.info(f"Retrieved information for group {group_id}")
return result
else:
logger.warning(f"Failed to retrieve information for group {group_id}")
return None
return {
"id": getattr(result, "id", None),
"title": getattr(result, "title", None),
"username": getattr(result, "username", None),
}
except Exception as e:
logger.error(f"Error retrieving group info for {group_id}: {e}")

View File

@@ -33,7 +33,7 @@ class MongoDBService:
def get_users(self) -> List[User]:
"""Retrieve all users from MongoDB collection"""
try:
if not self.collection:
if self.collection is None:
logger.error("MongoDB collection not initialized")
return []
@@ -48,7 +48,7 @@ class MongoDBService:
def get_user_by_id(self, user_id: int) -> Optional[User]:
"""Retrieve a specific user by ID from MongoDB"""
try:
if not self.collection:
if self.collection is None:
logger.error("MongoDB collection not initialized")
return None
@@ -63,7 +63,7 @@ class MongoDBService:
def save_user(self, user: User) -> bool:
"""Save a user to MongoDB collection"""
try:
if not self.collection:
if self.collection is None:
logger.error("MongoDB collection not initialized")
return False
@@ -84,7 +84,7 @@ class MongoDBService:
def delete_user(self, user_id: int) -> bool:
"""Delete a user from MongoDB collection"""
try:
if not self.collection:
if self.collection is None:
logger.error("MongoDB collection not initialized")
return False