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:
@@ -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=
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user