├── .env ├── requirements.txt ├── restart.sh ├── Restart.bat ├── LICENSE ├── README.md ├── docker-code.py └── Main.py /.env: -------------------------------------------------------------------------------- 1 | ROBLOSECURITY="" 2 | PORT=11254 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | pydantic 4 | httpx 5 | python-dotenv 6 | httpx[http2] 7 | fake-useragent 8 | -------------------------------------------------------------------------------- /restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if command -v python >/dev/null 2>&1; then 4 | PYTHON_CMD="python" 5 | elif command -v python3 >/dev/null 2>&1; then 6 | PYTHON_CMD="python3" 7 | else 8 | echo "Neither python nor python3 found. Exiting." 9 | exit 1 10 | fi 11 | 12 | while true; do 13 | echo "$(date) restarting" 14 | 15 | pkill -f "Main.py" && echo "Killed existing Main.py process." 16 | 17 | $PYTHON_CMD Main.py & 18 | echo "$(date) started new Main.py" 19 | 20 | sleep 700 21 | done 22 | -------------------------------------------------------------------------------- /Restart.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal enabledelayedexpansion 3 | 4 | where python >nul 2>&1 5 | if %errorlevel%==0 ( 6 | set PYTHON_CMD=python 7 | ) else ( 8 | where python3 >nul 2>&1 9 | if %errorlevel%==0 ( 10 | set PYTHON_CMD=python3 11 | ) else ( 12 | echo Neither python nor python3 found. Exiting. 13 | pause 14 | exit /b 15 | ) 16 | ) 17 | 18 | :loop 19 | echo [%date% %time%] Restarting Main.py 20 | 21 | for /f "tokens=2" %%a in ('tasklist ^| findstr /i Main.py') do ( 22 | echo Killing process %%a 23 | taskkill /PID %%a /F 24 | ) 25 | 26 | start "" %PYTHON_CMD% Main.py 27 | 28 | echo [%date% %time%] Started new Main.py 29 | timeout /t 700 /nobreak >nul 30 | goto loop -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Sentinel Team 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Roblox Group Management API 3 | 4 |

5 | Python Version 6 | Framework 7 | License 8 |

9 | 10 | This is a powerful, security-focused, and stealthy Roblox Group Management API written entirely in Python. It runs a local web server that wraps complex Roblox group operations into simple, secure RESTful API endpoints, allowing for easy automation of your group's administration. 11 | 12 | The core design philosophy of this project is the **"Paranoid Masquerade Mode,"** which aims to minimize the risk of detection by Roblox's anti-bot systems by deeply emulating the behavior of a real human user browsing the site. 13 | 14 | ## 🚀 Core Features: Why This is Different 15 | 16 | | Feature | This Project | Traditional / Simple Scripts | 17 | | :----------------------- | :----------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------- | 18 | | **Browser Simulation** | ✅ **High-Fidelity Mimicry**: Rotates browser identities & sends matching `sec-ch-ua` headers. | ❌ **Basic or None**: Static User-Agent, easy to fingerprint. | 19 | | **Session Management** | ✅ **Smart Keep-Alive**: Refreshes CSRF and session by simulating harmless activity. | ❌ **Passive**: Cookies & tokens expire quickly. | 20 | | **Request Protocol** | ✅ **Modern HTTP/2**: Uses `httpx` to match modern browser behavior. | ❌ **Outdated HTTP/1.1**. | 21 | | **Behavioral Pattern** | ✅ **Human-like Pacing**: Randomized delays to mimic human reaction times. | ❌ **Instantaneous** requests at machine speed. | 22 | | **Risk Profile** | 23 | 📋 Features List (API Endpoints) 24 | 25 | Information 26 | 27 | GET /groups/{group_id} – Get public information about a group. 28 | 29 | GET /groups/{group_id}/users/{user_identifier}/role – Get a user's role & rank in the group. 30 | 31 | GET /groups/{group_id}/roles – Get all roles (ID, name, rank). 32 | 33 | 34 | Ranking 35 | 36 | POST /groups/{group_id}/users/{user_identifier}/promote – Promote user. 37 | 38 | POST /groups/{group_id}/users/{user_identifier}/demote – Demote user. 39 | 40 | PATCH /groups/{group_id}/users/{user_identifier}/rank – Set user rank (1-255). 41 | 42 | POST /groups/{group_id}/mass-rank – Rank multiple users with reporting. 43 | 44 | 45 | Membership 46 | 47 | DELETE /groups/{group_id}/users/{user_identifier} – Kick user. 48 | 49 | POST /groups/{group_id}/kick-all – Kick all kickable members. 50 | 51 | GET /groups/{group_id}/join-requests – View join requests. 52 | 53 | POST /groups/{group_id}/join-requests/accept – Accept join request. 54 | 55 | POST /groups/{group_id}/join-requests/decline – Decline join request. 56 | 57 | GET /groups/{group_id}/members – List all members (paginated). 58 | 59 | GET /groups/{group_id}/roles/{role_id}/users – List users in a specific role. 60 | 61 | 62 | Finance 63 | 64 | GET /groups/{group_id}/revenue/summary/{time_period} – Revenue summary. 65 | 66 | POST /groups/{group_id}/payouts – Make payouts. 67 | 68 | 69 | Auditing 70 | 71 | GET /groups/{group_id}/audit-log – Group audit log. 72 | 73 | 74 | 75 | --- 76 | 77 | 🧰 Installation and Setup 78 | 79 | 1️⃣ Prerequisites 80 | 81 | Python 3.7+ 82 | 83 | 84 | 2️⃣ Install Dependencies 85 | 86 | pip install -r requirements.txt 87 | 88 | 3️⃣ Get Your .ROBLOSECURITY Cookie 89 | 90 | ⚠️ Security Warning: This cookie gives full account access. Never share it. 91 | 92 | Steps in Chrome/Edge: 93 | 94 | Log in to Roblox. 95 | 96 | Press F12 → Application tab → Cookies > https://www.roblox.com 97 | 98 | Find .ROBLOSECURITY and copy its full value. 99 | 100 | 101 | 4️⃣ Configure the Script 102 | 103 | Create a file named .env and add: 104 | 105 | ROBLOSECURITY=PASTE_YOUR_COOKIE_VALUE_HERE 106 | 107 | 5️⃣ Run the Server 108 | 109 | bash restart.sh 110 | 111 | The server will display the address and your unique API Key. 112 | 113 | 114 | --- 115 | 116 | 🔗 How to Use the API 117 | 118 | Get Your API Key 119 | 120 | The first run will print your API key and save it in api_key.txt. 121 | 122 | 123 | Authorize Requests 124 | 125 | ✅ Add your API key via: 126 | 127 | Header: 128 | x-api-key: YOUR_API_KEY 129 | 130 | Or Query Param: 131 | ?api_key=YOUR_API_KEY 132 | 133 | 134 | Interactive Docs 135 | 136 | Open http://127.0.0.1:8000/docs for a full Swagger UI to test endpoints. 137 | 138 | Example: Get Group Info 139 | 140 | curl -X GET "http://127.0.0.1:8000/groups/1234567" \ 141 | -H "x-api-key: your_api_key_here" 142 | 143 | 144 | --- 145 | 146 | 🔒 Security Considerations 147 | 148 | ✅ API Key Protection: All endpoints require the key. 149 | ✅ Local Execution: Credentials stay on your machine. 150 | ⚠️ Evasion: No method is 100% foolproof against detection. 151 | 152 | 153 | --- 154 | 155 | 📜 Disclaimer 156 | 157 | This tool is intended for educational and research purposes only. 158 | Using automation may violate Roblox's Terms of Service. 159 | You assume all risks including account bans. The developer assumes no liability. 160 | -------------------------------------------------------------------------------- /docker-code.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Query, HTTPException 2 | from pydantic import BaseModel 3 | from selenium import webdriver 4 | from selenium.webdriver.chrome.options import Options 5 | import os 6 | import json 7 | import logging 8 | import random 9 | import asyncio 10 | from typing import Optional, Dict, Any 11 | from starlette.status import HTTP_403_FORBIDDEN, HTTP_429_TOO_MANY_REQUESTS 12 | 13 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 14 | 15 | app = FastAPI( 16 | title="Chrome Remote Controller for Roblox API", 17 | description="A FastAPI service to control Chrome via Selenium WebDriver for Roblox API operations.", 18 | version="1.0.0" 19 | ) 20 | 21 | 初始化 Selenium WebDriver 22 | 23 | def init_driver(): 24 | chrome_options = Options() 25 | chrome_options.add_argument("--headless") 26 | chrome_options.add_argument("--no-sandbox") 27 | chrome_options.add_argument("--disable-dev-shm-usage") 28 | chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36") 29 | 30 | driver = webdriver.Remote( 31 | command_executor="http://localhost:4444/wd/hub", 32 | options=chrome_options 33 | ) 34 | driver.implicitly_wait(10) 35 | 36 | driver.get("https://www.roblox.com") 37 | roblox_cookie = os.getenv("ROBLOSECURITY") 38 | if not roblox_cookie: 39 | driver.quit() 40 | raise ValueError("ROBLOSECURITY environment variable not set!") 41 | driver.add_cookie({"name": ".ROBLOSECURITY", "value": roblox_cookie}) 42 | driver.add_cookie({ 43 | "name": "RBXEventTrackerV2", 44 | "value": f"CreateDate={time.strftime('%m/%d/%Y %I:%M:%S %p')}&rbxid=&browserid={random.randint(100000000, 999999999)}" 45 | }) 46 | driver.add_cookie({ 47 | "name": "GuestData", 48 | "value": f"UserID=-{random.randint(100000000, 999999999)}" 49 | }) 50 | 51 | logging.info("Selenium WebDriver initialized with cookies.") 52 | return driver 53 | 54 | driver = init_driver() 55 | csrf_token: Optional[str] = None 56 | 57 | class RobloxAPIRequest(BaseModel): 58 | method: str = "GET" 59 | url: str 60 | headers: Optional[Dict[str, str]] = None 61 | body: Optional[Dict[str, Any]] = None 62 | 63 | class RobloxAPIResponse(BaseModel): 64 | status: int 65 | body: Any 66 | headers: Dict[str, str] 67 | 68 | async def human_delay(): 69 | delay = random.uniform(0.4, 1.2) 70 | await asyncio.sleep(delay) 71 | 72 | async def get_csrf_token(): 73 | global csrf_token 74 | logging.info("Attempting to refresh CSRF token...") 75 | try: 76 | loop = asyncio.get_event_loop() 77 | def sync_get_csrf(): 78 | script = """ 79 | return fetch('https://auth.roblox.com/v2/logout', { 80 | method: 'POST', 81 | headers: { 'Content-Type': 'application/json' }, 82 | credentials: 'include' 83 | }).then(response => { 84 | if (response.status === 403) { 85 | return response.headers.get('x-csrf-token'); 86 | } 87 | return response.text().then(text => { throw { status: response.status, text: text }; }); 88 | }); 89 | """ 90 | csrf = driver.execute_script(script) 91 | if csrf: 92 | return csrf 93 | raise Exception("No CSRF token found") 94 | csrf_token = await loop.run_in_executor(None, sync_get_csrf) 95 | logging.info(f"Successfully refreshed CSRF token: {csrf_token}") 96 | except Exception as e: 97 | logging.error(f"Failed to get CSRF token: {str(e)}") 98 | raise HTTPException(status_code=503, detail=f"Failed to get CSRF token: {str(e)}") 99 | 100 | @app.on_event("startup") 101 | async def startup_event(): 102 | global csrf_token 103 | # wair 104 | await get_csrf_token() 105 | # wait 106 | async def csrf_refresher(): 107 | while True: 108 | await asyncio.sleep(random.uniform(290, 310)) 109 | logging.info("[CSRF-Refresher] Performing scheduled CSRF token refresh.") 110 | try: 111 | await get_csrf_token() 112 | except Exception as e: 113 | logging.error(f"[CSRF-Refresher] Failed to refresh CSRF token: {str(e)}") 114 | asyncio.create_task(csrf_refresher()) 115 | 116 | @app.on_event("shutdown") 117 | async def shutdown_event(): 118 | driver.quit() 119 | logging.info("Selenium WebDriver session closed.") 120 | 121 | @app.get("/") 122 | async def root(): 123 | return {"message": "Chrome controller ready"} 124 | 125 | @app.get("/open_url") 126 | async def open_url(url: str = Query(...)): 127 | try: 128 | driver.get(url) 129 | await human_delay() 130 | return {"status": "success", "url": url} 131 | except Exception as e: 132 | raise HTTPException(status_code=500, detail=f"Failed to open URL: {str(e)}") 133 | 134 | @app.post("/api_request", response_model=RobloxAPIResponse) 135 | async def execute_api_request(request: RobloxAPIRequest): 136 | """ 137 | Execute a Roblox API request using Chrome via Selenium. 138 | """ 139 | global csrf_token 140 | if request.method.upper() not in ["GET", "HEAD", "OPTIONS"] and csrf_token is None: 141 | await get_csrf_token() 142 | await human_delay() 143 | 144 | try: 145 | loop = asyncio.get_event_loop() 146 | def sync_request(): 147 | headers = request.headers or { 148 | "Accept": "application/json, text/plain, */*", 149 | "Content-Type": "application/json;charset=UTF-8" if request.body else "text/plain", 150 | "Referer": "https://www.roblox.com" 151 | } 152 | if request.method.upper() not in ["GET", "HEAD", "OPTIONS"] and csrf_token: 153 | headers["X-CSRF-TOKEN"] = csrf_token 154 | 155 | script = f""" 156 | return fetch('{request.url}', {{ 157 | method: '{request.method}', 158 | headers: {json.dumps(headers)}, 159 | body: {json.dumps(request.body) if request.body else "undefined"}, 160 | credentials: 'include' 161 | }}).then(response => {{ 162 | return response.text().then(text => {{ 163 | return {{ 164 | status: response.status, 165 | headers: Object.fromEntries(response.headers.entries()), 166 | body: text 167 | }}; 168 | }}); 169 | }}).catch(error => {{ 170 | throw {{ status: 500, text: error.message }}; 171 | }}); 172 | """ 173 | response = driver.execute_script(script) 174 | try: 175 | response["body"] = json.loads(response["body"]) 176 | except json.JSONDecodeError: 177 | pass # 保留原始文本 178 | return response 179 | 180 | response = await loop.run_in_executor(None, sync_request) 181 | await human_delay() 182 | 183 | if response["status"] == 403 and ("Token Validation Failed" in str(response["body"]) or "Authorization has been denied" in str(response["body"])): 184 | logging.warning("CSRF token validation failed. Retrying with a new token...") 185 | await get_csrf_token() 186 | headers = request.headers or {} 187 | headers["X-CSRF-TOKEN"] = csrf_token 188 | response = await loop.run_in_executor(None, lambda: driver.execute_script(f""" 189 | return fetch('{request.url}', {{ 190 | method: '{request.method}', 191 | headers: {json.dumps(headers)}, 192 | body: {json.dumps(request.body) if request.body else "undefined"}, 193 | credentials: 'include' 194 | }}).then(response => {{ 195 | return response.text().then(text => {{ 196 | return {{ 197 | status: response.status, 198 | headers: Object.fromEntries(response.headers.entries()), 199 | body: text 200 | }}; 201 | }}); 202 | }}); 203 | """)) 204 | try: 205 | response["body"] = json.loads(response["body"]) 206 | except json.JSONDecodeError: 207 | pass 208 | 209 | if response["status"] == 429: 210 | raise HTTPException(status_code=HTTP_429_TOO_MANY_REQUESTS, detail="Rate limit exceeded.") 211 | 212 | return RobloxAPIResponse( 213 | status=response["status"], 214 | body=response["body"], 215 | headers=response["headers"] 216 | ) 217 | except Exception as e: 218 | raise HTTPException(status_code=500, detail=f"API request failed: {str(e)}") 219 | 220 | -------------------------------------------------------------------------------- /Main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | import string 4 | import logging 5 | import json 6 | from typing import Optional, List, Union 7 | import asyncio 8 | import random 9 | import time 10 | import re 11 | 12 | import httpx 13 | import uvicorn 14 | from fastapi import FastAPI, HTTPException, Depends, Request, Query, Path, Security, Body 15 | from fastapi.security import APIKeyQuery, APIKeyHeader 16 | from pydantic import BaseModel, Field 17 | from starlette.responses import JSONResponse 18 | from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_429_TOO_MANY_REQUESTS 19 | from dotenv import load_dotenv 20 | from fake_useragent import UserAgent, FakeUserAgentError 21 | 22 | load_dotenv() 23 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') 24 | API_KEY_FILE = "api_key.txt" 25 | 26 | def generate_api_key(length: int = 32) -> str: 27 | alphabet = string.ascii_letters + string.digits 28 | return ''.join(secrets.choice(alphabet) for _ in range(length)) 29 | 30 | def load_or_create_api_key() -> str: 31 | if os.path.exists(API_KEY_FILE): 32 | with open(API_KEY_FILE, "r") as f: key = f.read().strip() 33 | if len(key) >= 32: logging.info("API key loaded from file."); return key 34 | key = generate_api_key() 35 | with open(API_KEY_FILE, "w") as f: f.write(key) 36 | logging.info(f"New API key generated and saved: {key}") 37 | return key 38 | 39 | ROBLOSECURITY_COOKIE = os.getenv("ROBLOSECURITY") 40 | APP_PORT = int(os.getenv("PORT", 8000)) 41 | if not ROBLOSECURITY_COOKIE: raise ValueError("ROBLOSECURITY environment variable not set!") 42 | API_KEY = load_or_create_api_key() 43 | 44 | class RobloxAPIError(Exception): 45 | def __init__(self, status_code: int, message: str): 46 | self.status_code = status_code; self.message = message 47 | super().__init__(f"Roblox API Error {status_code}: {message}") 48 | 49 | class RobloxClient: 50 | def __init__(self, cookie: str): 51 | self._cookie = cookie 52 | self._csrf_token: Optional[str] = None 53 | 54 | # --- Dynamic Identity Generation --- 55 | try: 56 | ua = UserAgent() 57 | user_agent = ua.random 58 | except FakeUserAgentError: 59 | # Fallback in case the user-agent service is down 60 | user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" 61 | logging.warning("fake-useragent service failed. Using a fallback User-Agent.") 62 | 63 | client_hints = self._generate_client_hints(user_agent) 64 | 65 | self._base_headers = { 66 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,application/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 67 | 'Accept-Encoding': 'gzip, deflate, br', 68 | 'Accept-Language': 'en-US,en;q=0.9', 69 | 'Sec-Ch-Ua-Mobile': '?0', 70 | 'Sec-Fetch-Dest': 'document', 71 | 'Sec-Fetch-Mode': 'navigate', 72 | 'Sec-Fetch-Site': 'none', 73 | 'Sec-Fetch-User': '?1', 74 | 'Upgrade-Insecure-Requests': '1', 75 | 'User-Agent': user_agent, 76 | 'Sec-Ch-Ua': client_hints["sec_ch_ua"], 77 | 'Sec-Ch-Ua-Platform': client_hints["sec_ch_ua_platform"], 78 | } 79 | 80 | # --- Enhanced Dynamic Cookie Generation --- 81 | random_past_seconds = random.randint(3600, 86400 * 3) # 1 hour to 3 days ago 82 | create_timestamp = time.time() - random_past_seconds 83 | create_date_str = time.strftime('%m/%d/%Y %I:%M:%S %p', time.localtime(create_timestamp)) 84 | 85 | self._base_cookies = { 86 | ".ROBLOSECURITY": self._cookie, 87 | "RBXEventTrackerV2": f"CreateDate={create_date_str}&rbxid=&browserid={random.randint(100000000, 999999999)}", 88 | "GuestData": f"UserID=-{random.randint(100000000, 999999999)}", 89 | "RBXSessionTracker": f"SessionTrackerID_{secrets.token_hex(16)}", 90 | "RBX-Id": str(random.randint(1000000000, 9999999999)), 91 | } 92 | 93 | self._last_url: Optional[str] = None 94 | 95 | self._session = httpx.AsyncClient( 96 | cookies=self._base_cookies, 97 | headers=self._base_headers, 98 | timeout=30.0, 99 | follow_redirects=True, 100 | http2=True, 101 | ) 102 | logging.info(f"RobloxClient initialized with dynamic identity. User-Agent: {user_agent}") 103 | logging.info("Mode: Paranoid (HTTP/2, Dynamic Identity, Referer Tracking, Human Pacing, Realistic Cookies)") 104 | 105 | def _generate_client_hints(self, user_agent: str) -> dict: 106 | platform = '"Windows"' 107 | if "Macintosh" in user_agent or "macOS" in user_agent: 108 | platform = '"macOS"' 109 | elif "Linux" in user_agent: 110 | platform = '"Linux"' 111 | elif "Android" in user_agent: 112 | platform = '"Android"' 113 | elif "iPhone" in user_agent or "iPad" in user_agent: 114 | platform = '"iOS"' 115 | 116 | chrome_version = re.search(r"Chrome/(\d+)", user_agent) 117 | edge_version = re.search(r"Edg/(\d+)", user_agent) 118 | firefox_version = re.search(r"Firefox/(\d+)", user_agent) 119 | 120 | sec_ch_ua = '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"' 121 | 122 | if edge_version: 123 | v = edge_version.group(1) 124 | sec_ch_ua = f'"Not_A Brand";v="8", "Chromium";v="{v}", "Microsoft Edge";v="{v}"' 125 | elif chrome_version: 126 | v = chrome_version.group(1) 127 | sec_ch_ua = f'"Not_A Brand";v="8", "Chromium";v="{v}", "Google Chrome";v="{v}"' 128 | elif firefox_version: 129 | # Firefox doesn't typically send Sec-CH-UA headers, but we can create a plausible non-value 130 | sec_ch_ua = '"Not_A Brand";v="99"' 131 | 132 | return {"sec_ch_ua": sec_ch_ua, "sec_ch_ua_platform": platform} 133 | 134 | async def close_session(self): await self._session.aclose() 135 | 136 | async def _get_csrf_token(self): 137 | info_url = "https://auth.roblox.com/v2/logout" 138 | logging.info("Attempting to refresh CSRF token...") 139 | try: 140 | api_headers = self._base_headers.copy() 141 | api_headers['Content-Type'] = 'application/json' 142 | if self._last_url: api_headers['Referer'] = self._last_url 143 | 144 | response = await self._session.post(info_url, headers=api_headers) 145 | if response.status_code == 403 and "x-csrf-token" in response.headers: 146 | self._csrf_token = response.headers["x-csrf-token"] 147 | logging.info(f"Successfully refreshed CSRF token. HTTP Version: {response.http_version}") 148 | else: 149 | raise RobloxAPIError(response.status_code, f"Failed to get CSRF token. Unexpected response: {response.text}") 150 | except httpx.RequestError as e: 151 | raise RobloxAPIError(503, f"Network error when trying to get CSRF token: {e}") 152 | 153 | async def request(self, method: str, url: str, **kwargs): 154 | if method.upper() not in ["GET", "HEAD", "OPTIONS"] and self._csrf_token is None: 155 | await self._get_csrf_token() 156 | await self._human_delay() 157 | 158 | api_headers = self._base_headers.copy() 159 | api_headers['Accept'] = 'application/json, text/plain, */*' 160 | api_headers['Sec-Fetch-Site'] = 'same-origin' 161 | 162 | if self._last_url: 163 | api_headers['Referer'] = self._last_url 164 | 165 | if "json" in kwargs: 166 | api_headers['Content-Type'] = 'application/json;charset=UTF-8' 167 | 168 | if self._csrf_token and method.upper() not in ["GET", "HEAD", "OPTIONS"]: 169 | api_headers["x-csrf-token"] = self._csrf_token 170 | 171 | extra_headers = kwargs.pop("headers", {}) 172 | api_headers.update(extra_headers) 173 | 174 | try: 175 | response = await self._session.request(method, url, headers=api_headers, **kwargs) 176 | if response.is_success and method.upper() == 'GET': 177 | self._last_url = url 178 | response.raise_for_status() 179 | except httpx.HTTPStatusError as e: 180 | if e.response.status_code == 403 and ("Token Validation Failed" in e.response.text or "Authorization has been denied" in e.response.text): 181 | logging.warning("CSRF token validation failed. Retrying with a new token...") 182 | await self._get_csrf_token() 183 | await self._human_delay() 184 | api_headers["x-csrf-token"] = self._csrf_token 185 | response = await self._session.request(method, url, headers=api_headers, **kwargs) 186 | if response.is_success and method.upper() == 'GET': self._last_url = url 187 | response.raise_for_status() 188 | else: 189 | try: 190 | error_details = e.response.json() 191 | error_message = error_details.get("errors", [{}])[0].get("message", e.response.text) 192 | except (json.JSONDecodeError, IndexError, KeyError): 193 | error_message = e.response.text 194 | raise RobloxAPIError(e.response.status_code, error_message) 195 | 196 | if not response.text: return None 197 | return response.json() 198 | 199 | async def _human_delay(self): 200 | delay = random.uniform(0.4, 1.2) 201 | await asyncio.sleep(delay) 202 | 203 | async def _get_authenticated_user_id(self) -> int: 204 | logging.info("Fetching authenticated user ID...") 205 | try: 206 | response = await self.request("GET", "https://users.roblox.com/v1/users/authenticated") 207 | if response and response.get("id"): 208 | user_id = response["id"] 209 | logging.info(f"Authenticated user ID: {user_id}") 210 | return user_id 211 | raise RobloxAPIError(401, "Could not retrieve authenticated user ID. The ROBLOSECURITY cookie might be invalid or expired.") 212 | except RobloxAPIError as e: 213 | logging.error(f"Roblox API Error fetching authenticated user ID: {e.status_code} - {e.message}") 214 | raise 215 | except Exception as e: 216 | logging.error(f"Unexpected error fetching authenticated user ID: {e}", exc_info=True) 217 | raise RobloxAPIError(500, f"An unexpected error occurred while fetching authenticated user ID: {e}") 218 | 219 | class RankUpdatePayload(BaseModel): rank: int = Field(..., gt=0, le=255, description="The target rank value (1-255).") 220 | class PayoutRecipient(BaseModel): recipientId: int; recipientType: str = Field("User", description="Must be 'User'."); amount: int = Field(..., gt=0, description="Amount of Robux to pay.") 221 | class PayoutPayload(BaseModel): PayoutType: str = Field("FixedAmount", description="Must be 'FixedAmount'."); Recipients: List[PayoutRecipient] 222 | class UserActionPayload(BaseModel): userId: int = Field(..., description="The target user's ID.") 223 | class StatusResponse(BaseModel): status: str; detail: Optional[str] = None 224 | 225 | class MultiUserRankPayload(BaseModel): 226 | users: List[Union[int, str]] = Field(..., description="A list of user IDs or usernames to rank.") 227 | rank: int = Field(..., gt=0, le=255, description="The target rank value (1-255).") 228 | 229 | class SingleUserRankResult(BaseModel): 230 | user_identifier: Union[int, str] = Field(..., description="The original identifier provided for the user.") 231 | user_id: Optional[int] = Field(None, description="The resolved Roblox user ID, if successful.") 232 | status: str = Field(..., description="Status of the operation for this user (success, skipped, failed).") 233 | detail: str = Field(..., description="Detailed message about the outcome for this user.") 234 | success: bool = Field(..., description="True if the operation for this user was successful or skipped.") 235 | 236 | class MultiUserRankResponse(BaseModel): 237 | overall_status: str = Field(..., description="Overall status of the batch operation (success, partial_success, failure).") 238 | results: List[SingleUserRankResult] = Field(..., description="A list of results for each user processed.") 239 | 240 | class SingleUserKickResult(BaseModel): 241 | user_id: int = Field(..., description="The Roblox user ID.") 242 | username: Optional[str] = Field(None, description="The Roblox username, if available.") 243 | status: str = Field(..., description="Status of the operation for this user (success, skipped, failed).") 244 | detail: str = Field(..., description="Detailed message about the outcome for this user.") 245 | success: bool = Field(..., description="True if the operation for this user was successful or skipped.") 246 | 247 | class MultiUserKickResponse(BaseModel): 248 | overall_status: str = Field(..., description="Overall status of the batch operation (success, partial_success, failure, skipped_all).") 249 | total_members_attempted: int = Field(..., description="Total number of members found and attempted to kick.") 250 | successful_kicks: int = Field(..., description="Number of members successfully kicked.") 251 | skipped_members: int = Field(..., description="Number of members skipped (e.g., owner, bot itself).") 252 | failed_kicks: int = Field(..., description="Number of members that failed to be kicked.") 253 | results: List[SingleUserKickResult] = Field(..., description="A list of results for each user processed.") 254 | 255 | 256 | api_key_query = APIKeyQuery(name="api_key", auto_error=False, description="Browser-friendly API Key (in URL).") 257 | api_key_header = APIKeyHeader(name="x-api-key", auto_error=False, description="Standard API Key (in request header).") 258 | async def get_api_key(key_from_query: str = Security(api_key_query), key_from_header: str = Security(api_key_header)): 259 | if key_from_header and secrets.compare_digest(key_from_header, API_KEY): return key_from_header 260 | if key_from_query and secrets.compare_digest(key_from_query, API_KEY): return key_from_query 261 | raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Invalid or missing API Key.") 262 | 263 | app = FastAPI( 264 | title="Roblox Group Management API", 265 | description="The most advanced pure-Python masquerading API for Roblox group management.", 266 | version="4.9.9" 267 | ) 268 | roblox_client = RobloxClient(ROBLOSECURITY_COOKIE) 269 | keep_alive_task = None 270 | csrf_refresher_task = None 271 | 272 | async def csrf_token_refresher(): 273 | await asyncio.sleep(10) 274 | while True: 275 | sleep_duration = random.uniform(290, 310) 276 | logging.info(f"[CSRF-Refresher] Next scheduled refresh in {sleep_duration / 60:.2f} minutes.") 277 | await asyncio.sleep(sleep_duration) 278 | 279 | logging.info("[CSRF-Refresher] Performing scheduled CSRF token refresh.") 280 | max_retries = 4 281 | retry_delay = 15 282 | 283 | for attempt in range(max_retries): 284 | try: 285 | await roblox_client._get_csrf_token() 286 | logging.info("[CSRF-Refresher] Proactive token refresh successful.") 287 | break 288 | except Exception as e: 289 | logging.error(f"[CSRF-Refresher] Refresh attempt {attempt + 1}/{max_retries} failed: {e}") 290 | if attempt < max_retries - 1: 291 | logging.info(f"[CSRF-Refresher] Retrying in {retry_delay} seconds...") 292 | await asyncio.sleep(retry_delay) 293 | retry_delay *= 2 294 | else: 295 | logging.critical("[CSRF-Refresher] All proactive refresh attempts failed. Token may be stale. The API will attempt to recover on the next necessary request.") 296 | 297 | async def _keep_alive_action_visit_home(): 298 | logging.info("[Keep-Alive] Action: Visiting roblox.com/home") 299 | response = await roblox_client._session.get("https://www.roblox.com/home") 300 | response.raise_for_status() 301 | logging.info(f"[Keep-Alive] Home page visit successful. HTTP Version: {response.http_version}") 302 | roblox_client._last_url = "https://www.roblox.com/home" 303 | 304 | async def _keep_alive_action_check_transactions(): 305 | logging.info("[Keep-Alive] Action: Checking transactions page") 306 | await roblox_client.request("GET", "https://economy.roblox.com/v2/users/1/transactions?transactionType=summary") 307 | 308 | async def _keep_alive_action_get_auth_user(): 309 | logging.info("[Keep-Alive] Action: Pinging authenticated user endpoint") 310 | await roblox_client.request("GET", "https://users.roblox.com/v1/users/authenticated") 311 | 312 | async def session_keep_alive(): 313 | await asyncio.sleep(15) 314 | possible_actions = [_keep_alive_action_visit_home, _keep_alive_action_check_transactions, _keep_alive_action_get_auth_user] 315 | while True: 316 | try: 317 | action = random.choice(possible_actions) 318 | await action() 319 | await roblox_client._human_delay() 320 | logging.info("[Keep-Alive] Action successful. Session appears active.") 321 | except Exception as e: 322 | logging.error(f"[Keep-Alive] An unexpected error occurred: {e}", exc_info=True) 323 | sleep_duration = random.uniform(900, 1500) 324 | logging.info(f"[Keep-Alive] Next check in {sleep_duration / 60:.2f} minutes.") 325 | await asyncio.sleep(sleep_duration) 326 | 327 | @app.on_event("startup") 328 | async def startup_event(): 329 | global keep_alive_task, csrf_refresher_task 330 | loop = asyncio.get_event_loop() 331 | logging.info("Starting session keep-alive background task.") 332 | keep_alive_task = loop.create_task(session_keep_alive()) 333 | logging.info("Starting periodic CSRF token refresher task (5 min interval).") 334 | csrf_refresher_task = loop.create_task(csrf_token_refresher()) 335 | 336 | @app.on_event("shutdown") 337 | async def shutdown_event(): 338 | if keep_alive_task: 339 | logging.info("Stopping session keep-alive background task.") 340 | keep_alive_task.cancel() 341 | try: await keep_alive_task 342 | except asyncio.CancelledError: logging.info("Keep-alive task successfully cancelled.") 343 | 344 | if csrf_refresher_task: 345 | logging.info("Stopping CSRF token refresher task.") 346 | csrf_refresher_task.cancel() 347 | try: await csrf_refresher_task 348 | except asyncio.CancelledError: logging.info("CSRF refresher task successfully cancelled.") 349 | 350 | await roblox_client.close_session() 351 | logging.info("RobloxClient session closed.") 352 | 353 | @app.exception_handler(RobloxAPIError) 354 | async def roblox_api_exception_handler(request: Request, exc: RobloxAPIError): 355 | try: 356 | roblox_details = json.loads(exc.message) 357 | except (json.JSONDecodeError, TypeError): 358 | roblox_details = exc.message 359 | 360 | client_status_code: int 361 | if exc.status_code == 401: 362 | client_status_code = HTTP_401_UNAUTHORIZED 363 | elif exc.status_code == 403: 364 | client_status_code = HTTP_403_FORBIDDEN 365 | elif exc.status_code == 404: 366 | client_status_code = HTTP_404_NOT_FOUND 367 | elif exc.status_code == 429: 368 | client_status_code = HTTP_429_TOO_MANY_REQUESTS 369 | elif 400 <= exc.status_code < 500: 370 | client_status_code = exc.status_code 371 | else: 372 | client_status_code = 502 373 | 374 | logging.warning(f"Mapping Roblox API error with status {exc.status_code} to client response {client_status_code}. Details: {roblox_details}") 375 | 376 | return JSONResponse( 377 | status_code=client_status_code, 378 | content={ 379 | "status": "error", 380 | "detail": "An upstream error occurred with the Roblox API.", 381 | "upstream_status": exc.status_code, 382 | "roblox_response": roblox_details, 383 | }, 384 | ) 385 | 386 | async def _get_id_from_username(username: str) -> int: 387 | await roblox_client._human_delay() 388 | url = "https://users.roblox.com/v1/usernames/users" 389 | payload = {"usernames": [username], "excludeBannedUsers": True} 390 | logging.info(f"Resolving username '{username}' to ID...") 391 | response = await roblox_client.request("POST", url, json=payload) 392 | if response and response.get("data") and len(response["data"]) > 0: 393 | user_id = response["data"][0]["id"] 394 | logging.info(f"Resolved username '{username}' to ID: {user_id}") 395 | return user_id 396 | raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"User with username '{username}' not found.") 397 | 398 | async def resolve_user_identifier(user_identifier: str = Path(..., description="The user's ID or username.")) -> int: 399 | if user_identifier.isdigit(): return int(user_identifier) 400 | return await _get_id_from_username(user_identifier) 401 | 402 | async def _get_group_roles(group_id: int) -> List[dict]: 403 | await roblox_client._human_delay() 404 | roles_url = f"https://groups.roblox.com/v1/groups/{group_id}/roles" 405 | logging.info(f"Fetching all roles for group {group_id}.") 406 | response_data = await roblox_client.request("GET", roles_url) 407 | if not response_data or "roles" not in response_data: 408 | raise HTTPException(status_code=502, detail="Could not retrieve roles from Roblox API, or response was malformed.") 409 | return response_data["roles"] 410 | 411 | async def _get_user_current_role(group_id: int, user_id: int) -> dict: 412 | await roblox_client._human_delay() 413 | url = f"https://groups.roblox.com/v2/users/{user_id}/groups/roles" 414 | logging.info(f"Fetching current role for user {user_id} in group {group_id}.") 415 | try: 416 | response_data = await roblox_client.request("GET", url) 417 | if not response_data or "data" not in response_data: 418 | raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"Could not retrieve role for user {user_id} in group {group_id}. User may not be a member.") 419 | for group_data in response_data["data"]: 420 | if group_data.get("group", {}).get("id") == group_id: 421 | if group_data.get("role") is None: 422 | return {"name": "Guest", "rank": 0} 423 | return group_data["role"] 424 | return {"name": "Guest", "rank": 0} 425 | except RobloxAPIError as e: 426 | if e.status_code == 400: 427 | raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail=f"User with ID {user_id} likely does not exist or is invalid.") 428 | raise e 429 | 430 | @app.get("/groups/{group_id}", tags=["Information"]) 431 | async def get_group_info(group_id: int = Path(..., description="The ID of the group to query."), _=Security(get_api_key)): 432 | """ 433 | Gets public information about a group, such as its name, owner, member count, and description. 434 | This endpoint does not require the authenticated user to be a member of the group. 435 | """ 436 | logging.info(f"Fetching basic information for group {group_id}.") 437 | await roblox_client._human_delay() 438 | url = f"https://groups.roblox.com/v1/groups/{group_id}" 439 | return await roblox_client.request("GET", url) 440 | 441 | @app.get("/groups/{group_id}/users/{user_identifier}/role", tags=["Information"]) 442 | async def get_user_role_in_group(group_id: int, user_id: int = Depends(resolve_user_identifier), _=Security(get_api_key)): 443 | """ 444 | Gets a specific user's current role and rank within a group. Essential for bot permission checks. 445 | """ 446 | logging.info(f"Explicitly fetching role for user {user_id} in group {group_id}.") 447 | return await _get_user_current_role(group_id, user_id) 448 | 449 | @app.get("/groups/{group_id}/roles", tags=["Information"]) 450 | async def get_group_roles(group_id: int = Path(..., description="The ID of the group."), _=Security(get_api_key)): 451 | """ 452 | Gets a list of all roles in a group, including their ID, name, and rank (0-255). 453 | """ 454 | return await _get_group_roles(group_id) 455 | 456 | @app.post("/groups/{group_id}/users/{user_identifier}/promote", response_model=StatusResponse, tags=["Ranking"]) 457 | async def promote_user(group_id: int, user_id: int = Depends(resolve_user_identifier), _=Security(get_api_key)): 458 | all_roles = await _get_group_roles(group_id) 459 | await roblox_client._human_delay() 460 | current_role = await _get_user_current_role(group_id, user_id) 461 | current_rank_value = current_role.get("rank", 0) 462 | 463 | if current_rank_value == 0: raise HTTPException(status_code=400, detail="User is not in the group and cannot be promoted.") 464 | if current_rank_value == 255: raise HTTPException(status_code=400, detail="Cannot promote the group owner.") 465 | 466 | sorted_roles = sorted([r for r in all_roles if r.get("rank", 0) > 0], key=lambda r: r["rank"]) 467 | 468 | next_role = next((role for role in sorted_roles if role["rank"] > current_rank_value), None) 469 | if next_role is None: raise HTTPException(status_code=400, detail="User is already at the highest promotable rank.") 470 | 471 | await roblox_client._human_delay() 472 | set_rank_url = f"https://groups.roblox.com/v1/groups/{group_id}/users/{user_id}" 473 | await roblox_client.request("PATCH", set_rank_url, json={"roleId": next_role["id"]}) 474 | return {"status": "success", "detail": f"User {user_id} successfully promoted from '{current_role.get('name', 'N/A')}' (Rank {current_rank_value}) to '{next_role['name']}' (Rank {next_role['rank']})."} 475 | 476 | @app.post("/groups/{group_id}/users/{user_identifier}/demote", response_model=StatusResponse, tags=["Ranking"]) 477 | async def demote_user(group_id: int, user_id: int = Depends(resolve_user_identifier), _=Security(get_api_key)): 478 | all_roles = await _get_group_roles(group_id) 479 | await roblox_client._human_delay() 480 | current_role = await _get_user_current_role(group_id, user_id) 481 | current_rank_value = current_role.get("rank", 0) 482 | 483 | if current_rank_value == 0: raise HTTPException(status_code=400, detail="User is not in the group and cannot be demoted.") 484 | if current_rank_value == 255: raise HTTPException(status_code=400, detail="Cannot demote the group owner.") 485 | 486 | sorted_roles = sorted([r for r in all_roles if r.get("rank", 0) > 0], key=lambda r: r["rank"], reverse=True) 487 | 488 | previous_role = next((role for role in sorted_roles if role["rank"] < current_rank_value), None) 489 | if previous_role is None: raise HTTPException(status_code=400, detail="User is already at the lowest rank.") 490 | 491 | await roblox_client._human_delay() 492 | set_rank_url = f"https://groups.roblox.com/v1/groups/{group_id}/users/{user_id}" 493 | await roblox_client.request("PATCH", set_rank_url, json={"roleId": previous_role["id"]}) 494 | return {"status": "success", "detail": f"User {user_id} successfully demoted from '{current_role.get('name', 'N/A')}' (Rank {current_rank_value}) to '{previous_role['name']}' (Rank {previous_role['rank']})."} 495 | 496 | @app.patch("/groups/{group_id}/users/{user_identifier}/rank", response_model=StatusResponse, tags=["Ranking"]) 497 | async def set_user_rank(group_id: int, payload: RankUpdatePayload, user_id: int = Depends(resolve_user_identifier), _=Security(get_api_key)): 498 | roles = await _get_group_roles(group_id) 499 | await roblox_client._human_delay() 500 | target_role = next((role for role in roles if role.get("rank") == payload.rank), None) 501 | if not target_role: raise HTTPException(status_code=404, detail=f"No role with rank '{payload.rank}' found in group {group_id}.") 502 | 503 | set_rank_url = f"https://groups.roblox.com/v1/groups/{group_id}/users/{user_id}" 504 | await roblox_client.request("PATCH", set_rank_url, json={"roleId": target_role["id"]}) 505 | return {"status": "success", "detail": f"User {user_id} rank successfully updated to {payload.rank} (Role: '{target_role['name']}')."} 506 | 507 | @app.delete("/groups/{group_id}/users/{user_identifier}", response_model=StatusResponse, tags=["Membership"]) 508 | async def kick_user_from_group(group_id: int, user_id: int = Depends(resolve_user_identifier), _=Security(get_api_key)): 509 | url = f"https://groups.roblox.com/v1/groups/{group_id}/users/{user_id}" 510 | await roblox_client.request("DELETE", url) 511 | return {"status": "success", "detail": f"User {user_id} has been successfully kicked from group {group_id}."} 512 | 513 | @app.get("/groups/{group_id}/members", tags=["Membership"]) 514 | async def get_group_members(group_id: int, limit: int = Query(100, ge=10, le=100), cursor: Optional[str] = Query(None), sort_order: str = Query("Asc", enum=["Asc", "Desc"]), _=Security(get_api_key)): 515 | url = f"https://groups.roblox.com/v1/groups/{group_id}/users?limit={limit}&sortOrder={sort_order}" 516 | if cursor: url += f"&cursor={cursor}" 517 | return await roblox_client.request("GET", url) 518 | 519 | @app.get("/groups/{group_id}/roles/{role_id}/users", tags=["Membership"]) 520 | async def get_members_in_role(group_id: int, role_id: int, limit: int = Query(100, ge=10, le=100), cursor: Optional[str] = Query(None), sort_order: str = Query("Asc", enum=["Asc", "Desc"]), _=Security(get_api_key)): 521 | url = f"https://groups.roblox.com/v1/groups/{group_id}/roles/{role_id}/users?limit={limit}&sortOrder={sort_order}" 522 | if cursor: url += f"&cursor={cursor}" 523 | return await roblox_client.request("GET", url) 524 | 525 | @app.get("/groups/{group_id}/join-requests", tags=["Membership"]) 526 | async def get_join_requests(group_id: int, limit: int = Query(100, ge=10, le=100), cursor: Optional[str] = Query(None), sort_order: str = Query("Asc", enum=["Asc", "Desc"]), _=Security(get_api_key)): 527 | url = f"https://groups.roblox.com/v1/groups/{group_id}/join-requests?limit={limit}&sortOrder={sort_order}" 528 | if cursor: url += f"&cursor={cursor}" 529 | return await roblox_client.request("GET", url) 530 | 531 | @app.post("/groups/{group_id}/join-requests/accept", response_model=StatusResponse, tags=["Membership"]) 532 | async def accept_group_join_request(group_id: int, payload: UserActionPayload, _=Security(get_api_key)): 533 | url = f"https://groups.roblox.com/v1/groups/{group_id}/join-requests/users/{payload.userId}" 534 | await roblox_client.request("POST", url, json={}) 535 | return {"status": "success", "detail": f"Accepted join request for user {payload.userId} in group {group_id}."} 536 | 537 | @app.post("/groups/{group_id}/join-requests/decline", response_model=StatusResponse, tags=["Membership"]) 538 | async def decline_group_join_request(group_id: int, payload: UserActionPayload, _=Security(get_api_key)): 539 | url = f"https://groups.roblox.com/v1/groups/{group_id}/join-requests/users/{payload.userId}" 540 | await roblox_client.request("DELETE", url) 541 | return {"status": "success", "detail": f"Declined join request for user {payload.userId} in group {group_id}."} 542 | 543 | @app.get("/groups/{group_id}/revenue/summary/{time_period}", tags=["Finance"]) 544 | async def get_group_funds(group_id: int, time_period: str = Path(..., enum=["Day", "Week", "Month", "Year"]), _=Security(get_api_key)): 545 | url = f"https://economy.roblox.com/v1/groups/{group_id}/revenue/summary/{time_period}" 546 | return await roblox_client.request("GET", url) 547 | 548 | @app.post("/groups/{group_id}/payouts", response_model=StatusResponse, tags=["Finance"]) 549 | async def make_payout(group_id: int, payload: PayoutPayload, _=Security(get_api_key)): 550 | url = f"https://groups.roblox.com/v1/groups/{group_id}/payouts" 551 | await roblox_client.request("POST", url, json=payload.dict()) 552 | return {"status": "success", "detail": "Payout request processed successfully."} 553 | 554 | @app.get("/groups/{group_id}/audit-log", tags=["Auditing"]) 555 | async def get_audit_log(group_id: int, action_type: str = Query("All"), user_id: Optional[int] = Query(None), limit: int = Query(100, ge=10, le=100), cursor: Optional[str] = Query(None), sort_order: str = Query("Asc", enum=["Asc", "Desc"]), _=Security(get_api_key)): 556 | url = f"https://groups.roblox.com/v1/groups/{group_id}/audit-log?limit={limit}&sortOrder={sort_order}&actionType={action_type}" 557 | if user_id: url += f"&userId={user_id}" 558 | if cursor: url += f"&cursor={cursor}" 559 | return await roblox_client.request("GET", url) 560 | 561 | @app.post("/groups/{group_id}/mass-rank", response_model=MultiUserRankResponse, tags=["Ranking"]) 562 | async def set_multiple_user_ranks( 563 | group_id: int = Path(..., description="The ID of the group."), 564 | payload: MultiUserRankPayload = Body(..., description="List of users to rank and the target rank."), 565 | _=Security(get_api_key) 566 | ): 567 | """ 568 | Sets a specific rank for multiple users within a group. 569 | 570 | The bot (the authenticated account via ROBLOSECURITY cookie) must have the necessary 571 | permissions to rank users in the target group. 572 | 573 | **Constraints:** 574 | - Cannot set a rank higher than the bot's own rank in the group (unless the bot is the group owner). 575 | - Cannot set the rank to 255 (Owner) unless the bot itself is the group owner. 576 | - Cannot modify the rank of users who are currently ranked higher than the bot. 577 | - Users not in the group will be skipped or result in an error if their username cannot be resolved. 578 | - The authenticated bot itself will be skipped if included in the list. 579 | """ 580 | logging.info(f"Received mass rank request for group {group_id} to rank {payload.rank} for {len(payload.users)} users.") 581 | results: List[SingleUserRankResult] = [] 582 | 583 | try: 584 | authenticated_user_id = await roblox_client._get_authenticated_user_id() 585 | except RobloxAPIError as e: 586 | raise HTTPException(status_code=500, detail=f"Failed to identify the bot's own user ID: {e.message}") 587 | 588 | try: 589 | bot_current_role = await _get_user_current_role(group_id, authenticated_user_id) 590 | bot_rank = bot_current_role.get("rank", 0) 591 | except HTTPException as e: 592 | if "Could not retrieve role for user" in e.detail or "User with ID" in e.detail: 593 | raise HTTPException(status_code=403, detail=f"The authenticated account (ID: {authenticated_user_id}) is not a member of group {group_id} or could not fetch its role. Cannot perform ranking operations.") 594 | raise HTTPException(status_code=e.status_code, detail=f"Failed to get bot's role in group {group_id}: {e.detail}") 595 | 596 | if bot_rank == 0: 597 | raise HTTPException(status_code=403, detail=f"The authenticated account (ID: {authenticated_user_id}) is not ranked in group {group_id}. Cannot perform ranking operations as a guest.") 598 | 599 | logging.info(f"Bot's current rank in group {group_id}: {bot_rank}") 600 | 601 | all_roles = await _get_group_roles(group_id) 602 | target_role = next((role for role in all_roles if role.get("rank") == payload.rank), None) 603 | 604 | if not target_role: 605 | raise HTTPException(status_code=404, detail=f"No role with target rank '{payload.rank}' found in group {group_id}. Please ensure the target rank exists.") 606 | 607 | if payload.rank > bot_rank and bot_rank != 255: 608 | raise HTTPException(status_code=403, detail=f"Target rank ({payload.rank}) is higher than the bot's current rank ({bot_rank}). The bot cannot promote users to a rank higher than its own.") 609 | 610 | if payload.rank == 255 and bot_rank != 255: 611 | raise HTTPException(status_code=403, detail="Cannot set users to owner rank (255) unless the bot itself is the group owner.") 612 | 613 | for user_id_or_username in payload.users: 614 | user_result = SingleUserRankResult( 615 | user_identifier=user_id_or_username, 616 | status="failed", 617 | detail="Initial status.", 618 | success=False 619 | ) 620 | try: 621 | current_user_id = None 622 | if isinstance(user_id_or_username, int): 623 | current_user_id = user_id_or_username 624 | else: 625 | try: 626 | current_user_id = await _get_id_from_username(str(user_id_or_username)) 627 | except HTTPException as e: 628 | user_result.detail = f"Username '{user_id_or_username}' not found or could not be resolved: {e.detail}" 629 | logging.warning(user_result.detail) 630 | results.append(user_result) 631 | continue 632 | 633 | user_result.user_id = current_user_id 634 | 635 | current_user_role = {"name": "Guest", "rank": 0} 636 | try: 637 | current_user_role = await _get_user_current_role(group_id, current_user_id) 638 | except HTTPException as e: 639 | if "Could not retrieve role for user" in e.detail or "User with ID" in e.detail: 640 | current_user_role = {"name": "Not In Group", "rank": 0} 641 | logging.info(f"User {current_user_id} not found in group {group_id} (or not resolvable within group context), assuming rank 0.") 642 | else: 643 | raise 644 | 645 | current_user_rank = current_user_role.get("rank", 0) 646 | 647 | if current_user_id == authenticated_user_id: 648 | user_result.detail = f"User {current_user_id} is the authenticated bot itself. Skipping ranking operation for self." 649 | user_result.status = "skipped" 650 | user_result.success = True 651 | logging.info(user_result.detail) 652 | results.append(user_result) 653 | continue 654 | 655 | if current_user_rank == 255: 656 | user_result.detail = f"User {current_user_id} is the group owner (Rank 255) and cannot be ranked by this API." 657 | user_result.status = "skipped" 658 | user_result.success = True 659 | logging.warning(user_result.detail) 660 | results.append(user_result) 661 | continue 662 | 663 | if current_user_rank > bot_rank: 664 | user_result.detail = f"User {current_user_id} is currently rank {current_user_rank}, which is higher than the bot's rank ({bot_rank}). Cannot modify this user's rank." 665 | user_result.status = "skipped" 666 | user_result.success = True 667 | logging.warning(user_result.detail) 668 | results.append(user_result) 669 | continue 670 | 671 | if current_user_rank == payload.rank: 672 | user_result.status = "skipped" 673 | user_result.success = True 674 | user_result.detail = f"User {current_user_id} is already at target rank {payload.rank} ('{target_role['name']}')." 675 | logging.info(user_result.detail) 676 | results.append(user_result) 677 | continue 678 | 679 | logging.info(f"Attempting to set rank for user {current_user_id} from {current_user_rank} ('{current_user_role.get('name', 'N/A')}') to {payload.rank} ('{target_role['name']}') in group {group_id}.") 680 | await roblox_client._human_delay() 681 | set_rank_url = f"https://groups.roblox.com/v1/groups/{group_id}/users/{current_user_id}" 682 | await roblox_client.request("PATCH", set_rank_url, json={"roleId": target_role["id"]}) 683 | 684 | user_result.status = "success" 685 | user_result.success = True 686 | user_result.detail = f"User {current_user_id} successfully ranked to {payload.rank} ('{target_role['name']}')." 687 | logging.info(user_result.detail) 688 | 689 | except HTTPException as e: 690 | user_result.detail = f"API Error for {user_id_or_username}: {e.detail}" 691 | logging.error(f"API Error for user {user_id_or_username}: {e.detail}") 692 | except RobloxAPIError as e: 693 | user_result.detail = f"Roblox API Error ({e.status_code}) for {user_id_or_username}: {e.message}" 694 | logging.error(f"Roblox API Error for user {user_id_or_username}: {e.status_code} - {e.message}") 695 | except Exception as e: 696 | user_result.detail = f"Unexpected error for {user_id_or_username}: {str(e)}" 697 | logging.error(f"Unexpected error for user {user_id_or_username}: {str(e)}", exc_info=True) 698 | finally: 699 | results.append(user_result) 700 | 701 | success_count = sum(1 for r in results if r.success) 702 | failure_count = sum(1 for r in results if not r.success and r.status != 'skipped') 703 | 704 | overall_status = "success" 705 | if failure_count > 0 and success_count > 0: 706 | overall_status = "partial_success" 707 | elif failure_count > 0 and success_count == 0: 708 | overall_status = "failure" 709 | elif success_count == 0 and len(payload.users) > 0 and failure_count == 0: 710 | overall_status = "skipped_all" 711 | elif len(payload.users) == 0: 712 | overall_status = "success" 713 | 714 | logging.info(f"Mass rank operation finished for group {group_id}. Overall status: {overall_status}. Successful/Skipped: {success_count}, Failed: {failure_count}.") 715 | return MultiUserRankResponse(overall_status=overall_status, results=results) 716 | 717 | @app.post("/groups/{group_id}/kick-all", response_model=MultiUserKickResponse, tags=["Membership"]) 718 | async def kick_all_members( 719 | group_id: int = Path(..., description="The ID of the group from which to kick all members."), 720 | _=Security(get_api_key) 721 | ): 722 | """ 723 | Kicks all members from a Roblox group, excluding the group owner and the bot itself. 724 | 725 | The bot (the authenticated account via ROBLOSECURITY cookie) must have the necessary 726 | permissions to kick members in the target group. 727 | 728 | **Constraints:** 729 | - Cannot kick the group owner (rank 255). 730 | - Cannot kick the authenticated bot itself. 731 | - Cannot kick users who are currently ranked higher than the bot. 732 | - The bot must be a member of the group and have appropriate permissions. 733 | """ 734 | logging.info(f"Received request to kick all members from group {group_id}.") 735 | results: List[SingleUserKickResult] = [] 736 | total_members_attempted = 0 737 | successful_kicks = 0 738 | skipped_members = 0 739 | failed_kicks = 0 740 | 741 | try: 742 | authenticated_user_id = await roblox_client._get_authenticated_user_id() 743 | except RobloxAPIError as e: 744 | raise HTTPException(status_code=500, detail=f"Failed to identify the bot's own user ID: {e.message}. Cannot proceed with kicking.") 745 | 746 | try: 747 | bot_current_role = await _get_user_current_role(group_id, authenticated_user_id) 748 | bot_rank = bot_current_role.get("rank", 0) 749 | except HTTPException as e: 750 | if "Could not retrieve role for user" in e.detail or "User with ID" in e.detail: 751 | raise HTTPException(status_code=403, detail=f"The authenticated account (ID: {authenticated_user_id}) is not a member of group {group_id} or could not fetch its role. Cannot perform kicking operations.") 752 | raise HTTPException(status_code=e.status_code, detail=f"Failed to get bot's role in group {group_id}: {e.detail}") 753 | 754 | if bot_rank == 0: 755 | raise HTTPException(status_code=403, detail=f"The authenticated account (ID: {authenticated_user_id}) is not ranked in group {group_id}. Cannot perform kicking operations as a guest.") 756 | 757 | logging.info(f"Bot's current rank in group {group_id}: {bot_rank}") 758 | 759 | cursor: Optional[str] = None 760 | while True: 761 | logging.info(f"Fetching members page for group {group_id}, cursor: {cursor}") 762 | try: 763 | members_page = await roblox_client.request("GET", f"https://groups.roblox.com/v1/groups/{group_id}/users?limit=100&sortOrder=Asc&cursor={cursor or ''}") 764 | await roblox_client._human_delay() 765 | except RobloxAPIError as e: 766 | logging.error(f"Roblox API Error fetching members page (cursor: {cursor}): {e.status_code} - {e.message}") 767 | break 768 | 769 | if not members_page or "data" not in members_page or not members_page["data"]: 770 | logging.info("No more members data or empty page returned.") 771 | break 772 | 773 | for member_data in members_page["data"]: 774 | user_id = member_data["user"]["userId"] 775 | username = member_data["user"]["username"] 776 | member_rank = member_data["role"]["rank"] 777 | total_members_attempted += 1 778 | 779 | kick_result = SingleUserKickResult( 780 | user_id=user_id, 781 | username=username, 782 | status="failed", 783 | detail="Initial status.", 784 | success=False 785 | ) 786 | 787 | if user_id == authenticated_user_id: 788 | kick_result.status = "skipped" 789 | kick_result.success = True 790 | kick_result.detail = f"User {user_id} ('{username}') is the authenticated bot itself. Skipping kick operation for self." 791 | skipped_members += 1 792 | logging.info(kick_result.detail) 793 | results.append(kick_result) 794 | continue 795 | 796 | if member_rank == 255: 797 | kick_result.status = "skipped" 798 | kick_result.success = True 799 | kick_result.detail = f"User {user_id} ('{username}') is the group owner (Rank 255). Cannot kick owner." 800 | skipped_members += 1 801 | logging.warning(kick_result.detail) 802 | results.append(kick_result) 803 | continue 804 | 805 | if member_rank > bot_rank and bot_rank != 255: 806 | kick_result.status = "skipped" 807 | kick_result.success = True 808 | kick_result.detail = f"User {user_id} ('{username}') is currently rank {member_rank}, which is higher than the bot's rank ({bot_rank}). Cannot kick this user due to insufficient permissions." 809 | skipped_members += 1 810 | logging.warning(kick_result.detail) 811 | results.append(kick_result) 812 | continue 813 | 814 | 815 | logging.info(f"Attempting to kick user {user_id} ('{username}') from group {group_id}.") 816 | try: 817 | await roblox_client._human_delay() 818 | await roblox_client.request("DELETE", f"https://groups.roblox.com/v1/groups/{group_id}/users/{user_id}") 819 | kick_result.status = "success" 820 | kick_result.success = True 821 | kick_result.detail = f"User {user_id} ('{username}') successfully kicked." 822 | successful_kicks += 1 823 | logging.info(kick_result.detail) 824 | except RobloxAPIError as e: 825 | kick_result.detail = f"Roblox API Error ({e.status_code}) for {user_id} ('{username}'): {e.message}" 826 | failed_kicks += 1 827 | logging.error(f"Roblox API Error kicking user {user_id}: {e.status_code} - {e.message}") 828 | except Exception as e: 829 | kick_result.detail = f"Unexpected error for {user_id} ('{username}'): {str(e)}" 830 | failed_kicks += 1 831 | logging.error(f"Unexpected error kicking user {user_id}: {str(e)}", exc_info=True) 832 | finally: 833 | results.append(kick_result) 834 | 835 | cursor = members_page.get("nextPageCursor") 836 | if not cursor: 837 | break 838 | 839 | overall_status = "success" 840 | if failed_kicks > 0 and (successful_kicks > 0 or skipped_members > 0): 841 | overall_status = "partial_success" 842 | elif failed_kicks > 0 and successful_kicks == 0 and skipped_members == 0: 843 | overall_status = "failure" 844 | elif total_members_attempted == skipped_members and total_members_attempted > 0: 845 | overall_status = "skipped_all" 846 | elif total_members_attempted == 0: 847 | overall_status = "success" 848 | 849 | logging.info(f"Mass kick operation finished for group {group_id}. Overall status: {overall_status}. Total members found/attempted: {total_members_attempted}, Kicked: {successful_kicks}, Skipped: {skipped_members}, Failed: {failed_kicks}.") 850 | 851 | return MultiUserKickResponse( 852 | overall_status=overall_status, 853 | total_members_attempted=total_members_attempted, 854 | successful_kicks=successful_kicks, 855 | skipped_members=skipped_members, 856 | failed_kicks=failed_kicks, 857 | results=results 858 | ) 859 | 860 | if __name__ == "__main__": 861 | print("--- Roblox Group Management API [Paranoid Masquerade Edition - v4.9.9] ---") 862 | print("Starting server...") 863 | print("Please ensure you have run 'pip install fake-useragent' for dynamic identity generation.") 864 | print(f"Listening on: http://0.0.0.0:{APP_PORT}") 865 | print(f"API Documentation available at: http://127.0.0.1:{APP_PORT}/docs") 866 | print(f"Your API Key is: {API_KEY}") 867 | print("-----------------------------------") 868 | uvicorn.run(app, host="0.0.0.0", port=APP_PORT) --------------------------------------------------------------------------------