From f8d1713a1288b4637c2cfa1f1cb9df50440824d8 Mon Sep 17 00:00:00 2001 From: filoxenace Date: Sat, 7 Mar 2026 12:24:04 -0500 Subject: [PATCH] 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. --- .env.example | 4 ++++ README.md | 40 ++++++++++++++++++++++++++++++++++++++-- src/main.py | 49 ++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index eb6b614..aa8b9e3 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ ROBLOSECURITY_TOKEN= PUBLISHER_USER_ID= 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) ROBLOX_PROXY= diff --git a/README.md b/README.md index f2a6a68..b7a49f4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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 @@ -20,7 +20,7 @@ This will create a virtual environment and install all required packages. ## 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= # The Roblox group ID to upload clothing to @@ -32,6 +32,23 @@ DISCORD_WEBHOOK_URL= # (Optional) Discord webhook for upload notif **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= # Roblox cookie for the onsale account +ONSALE_PUBLISHER_USER_ID= # User ID for the onsale account +``` + +### Other optional settings + +``` +ROBLOX_PROXY= # 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 ### Development Server @@ -49,3 +66,22 @@ uv run fastapi run src/main.py ``` 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 diff --git a/src/main.py b/src/main.py index 7f70ab0..76f06a7 100644 --- a/src/main.py +++ b/src/main.py @@ -17,6 +17,10 @@ ROBLOSECURITY = os.getenv("ROBLOSECURITY_TOKEN") PUBLISHER_USER_ID = os.getenv("PUBLISHER_USER_ID") 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: raise EnvironmentError("TARGET_ID is missing from environment.") 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)) 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 async def lifespan(app: FastAPI): - global roblox - async with RobloxClient( - roblosecurity=ROBLOSECURITY, - publisher_user_id=int(PUBLISHER_USER_ID), - proxy=ROBLOX_PROXY, - ) as client: - roblox = client + async with _create_clients(): task = asyncio.create_task(process_onsale_queue()) yield task.cancel() @@ -66,7 +97,7 @@ async def process_onsale_queue(): f"Retrying onsale for asset {item['asset_id']} (Attempt {item['retry_count'] + 1})" ) try: - await roblox.onsale_asset( + await roblox_onsale.onsale_asset( item["asset_id"], item["name"], item["description"], @@ -176,7 +207,7 @@ async def reupload_asset(asset_id: int, _: str = Depends(verify_api_key)): await asyncio.sleep(5) try: - await roblox.onsale_asset( + await roblox_onsale.onsale_asset( new_asset_id, asset.name, new_description,