feat: support separate account for onsale operations

Add optional ONSALE_ROBLOSECURITY_TOKEN and ONSALE_PUBLISHER_USER_ID
env vars to use a different Roblox account for putting items on sale.
Falls back to the upload account when not set. Update README with
new config options and API endpoint docs.
This commit is contained in:
2026-03-07 12:24:04 -05:00
parent a761733f8c
commit f8d1713a12
3 changed files with 82 additions and 11 deletions
+4
View File
@@ -4,6 +4,10 @@ ROBLOSECURITY_TOKEN=
PUBLISHER_USER_ID= PUBLISHER_USER_ID=
DISCORD_WEBHOOK_URL= DISCORD_WEBHOOK_URL=
# Optional: Separate account for onsaling (defaults to upload account if not set)
ONSALE_ROBLOSECURITY_TOKEN=
ONSALE_PUBLISHER_USER_ID=
# Optional: Proxy for Roblox APIs (e.g., roproxy.com) # Optional: Proxy for Roblox APIs (e.g., roproxy.com)
ROBLOX_PROXY= ROBLOX_PROXY=
+38 -2
View File
@@ -1,6 +1,6 @@
# Threads # Threads
A FastAPI service for reuploading clothing assets to Roblox groups, used in *Filoxen Research Facilities*. This app fetches clothing from existing Roblox assets and reuploads them to a target group. A FastAPI service for reuploading clothing assets to Roblox groups, used in *Filoxen Research Facilities*. This app fetches clothing from existing Roblox assets, reuploads them to a target group, and automatically puts them on sale.
## Prerequisites ## Prerequisites
@@ -20,7 +20,7 @@ This will create a virtual environment and install all required packages.
## Configuration ## Configuration
Create a `.env` file in the project root with the following variables: Create a `.env` file in the project root (see `.env.example`):
``` ```
TARGET_ID=<group_id> # The Roblox group ID to upload clothing to TARGET_ID=<group_id> # The Roblox group ID to upload clothing to
@@ -32,6 +32,23 @@ DISCORD_WEBHOOK_URL=<url> # (Optional) Discord webhook for upload notif
**Important:** `PUBLISHER_USER_ID` must match the user ID of the account that owns the `ROBLOSECURITY_TOKEN`. **Important:** `PUBLISHER_USER_ID` must match the user ID of the account that owns the `ROBLOSECURITY_TOKEN`.
### Separate onsale account
You can optionally use a different Roblox account for putting items on sale. If not set, the upload account is used for both.
```
ONSALE_ROBLOSECURITY_TOKEN=<cookie> # Roblox cookie for the onsale account
ONSALE_PUBLISHER_USER_ID=<user_id> # User ID for the onsale account
```
### Other optional settings
```
ROBLOX_PROXY=<url> # Proxy for Roblox APIs (e.g., roproxy.com)
RETRY_INTERVAL_SECONDS=60 # How often to check the onsale retry queue (default: 60)
RETRY_DELAY_SECONDS=300 # Delay before retrying a failed onsale (default: 300)
```
## Running ## Running
### Development Server ### Development Server
@@ -49,3 +66,22 @@ uv run fastapi run src/main.py
``` ```
The server will run on port 8000 by default. The server will run on port 8000 by default.
## API Endpoints
All endpoints require an `x-api-key` header matching `VALID_API_KEY`.
### `GET /asset/{asset_id}`
Fetch info about a Roblox asset.
### `POST /create/?asset_id={asset_id}`
Reupload a clothing asset to the target group. The service will:
1. Fetch the original asset and its image
2. Deduplicate by image hash (returns existing upload if already processed)
3. Upload the clothing image to the target group
4. Wait 5 seconds, then put the item on sale
5. If rate-limited, queue the onsale for automatic retry
6. Send a Discord webhook notification on success
+40 -9
View File
@@ -17,6 +17,10 @@ ROBLOSECURITY = os.getenv("ROBLOSECURITY_TOKEN")
PUBLISHER_USER_ID = os.getenv("PUBLISHER_USER_ID") PUBLISHER_USER_ID = os.getenv("PUBLISHER_USER_ID")
ROBLOX_PROXY = os.getenv("ROBLOX_PROXY") ROBLOX_PROXY = os.getenv("ROBLOX_PROXY")
# Optional separate account for onsaling (falls back to upload account)
ONSALE_ROBLOSECURITY = os.getenv("ONSALE_ROBLOSECURITY_TOKEN", ROBLOSECURITY)
ONSALE_PUBLISHER_USER_ID = os.getenv("ONSALE_PUBLISHER_USER_ID", PUBLISHER_USER_ID)
if TARGET is None: if TARGET is None:
raise EnvironmentError("TARGET_ID is missing from environment.") raise EnvironmentError("TARGET_ID is missing from environment.")
if not VALID_API_KEY: if not VALID_API_KEY:
@@ -33,17 +37,44 @@ RETRY_INTERVAL = int(os.getenv("RETRY_INTERVAL_SECONDS", 60))
RETRY_DELAY = int(os.getenv("RETRY_DELAY_SECONDS", 300)) RETRY_DELAY = int(os.getenv("RETRY_DELAY_SECONDS", 300))
roblox: RobloxClient roblox: RobloxClient
roblox_onsale: RobloxClient
_use_separate_onsale = (
ONSALE_ROBLOSECURITY != ROBLOSECURITY
or ONSALE_PUBLISHER_USER_ID != PUBLISHER_USER_ID
)
@asynccontextmanager
async def _create_clients():
global roblox, roblox_onsale
if _use_separate_onsale:
async with RobloxClient(
roblosecurity=ROBLOSECURITY,
publisher_user_id=int(PUBLISHER_USER_ID),
proxy=ROBLOX_PROXY,
) as upload_client, RobloxClient(
roblosecurity=ONSALE_ROBLOSECURITY,
publisher_user_id=int(ONSALE_PUBLISHER_USER_ID),
proxy=ROBLOX_PROXY,
) as onsale_client:
roblox = upload_client
roblox_onsale = onsale_client
yield
else:
async with RobloxClient(
roblosecurity=ROBLOSECURITY,
publisher_user_id=int(PUBLISHER_USER_ID),
proxy=ROBLOX_PROXY,
) as client:
roblox = client
roblox_onsale = client
yield
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
global roblox async with _create_clients():
async with RobloxClient(
roblosecurity=ROBLOSECURITY,
publisher_user_id=int(PUBLISHER_USER_ID),
proxy=ROBLOX_PROXY,
) as client:
roblox = client
task = asyncio.create_task(process_onsale_queue()) task = asyncio.create_task(process_onsale_queue())
yield yield
task.cancel() task.cancel()
@@ -66,7 +97,7 @@ async def process_onsale_queue():
f"Retrying onsale for asset {item['asset_id']} (Attempt {item['retry_count'] + 1})" f"Retrying onsale for asset {item['asset_id']} (Attempt {item['retry_count'] + 1})"
) )
try: try:
await roblox.onsale_asset( await roblox_onsale.onsale_asset(
item["asset_id"], item["asset_id"],
item["name"], item["name"],
item["description"], item["description"],
@@ -176,7 +207,7 @@ async def reupload_asset(asset_id: int, _: str = Depends(verify_api_key)):
await asyncio.sleep(5) await asyncio.sleep(5)
try: try:
await roblox.onsale_asset( await roblox_onsale.onsale_asset(
new_asset_id, new_asset_id,
asset.name, asset.name,
new_description, new_description,