| import os, time |
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Response, Request |
| from fastapi.staticfiles import StaticFiles |
| from authlib.integrations.starlette_client import OAuth |
| from starlette.middleware.sessions import SessionMiddleware |
| from starlette.responses import RedirectResponse, JSONResponse |
| from fastapi.responses import FileResponse |
| from fastapi import Header |
| from fastapi import Query |
| import jwt |
| import datetime |
| import httpx |
|
|
| JWT_SECRET = os.environ.get("JWT_SECRET") |
|
|
| app = FastAPI() |
|
|
| |
| CLIENT_ID = os.environ.get("ROBLOX_CLIENT_ID") |
| CLIENT_SECRET = os.environ.get("ROBLOX_CLIENT_SECRET") |
| SESSION_KEY = os.environ.get("SESSION_SECRET", "tony_safe_2026") |
| WHITE_LIST = os.environ.get("WHITE_LIST", "").split(",") |
|
|
| app.add_middleware(SessionMiddleware, secret_key=SESSION_KEY) |
|
|
| oauth = OAuth() |
| oauth.register( |
| name='roblox', |
| client_id=CLIENT_ID, |
| client_secret=CLIENT_SECRET, |
| server_metadata_url='https://apis.roblox.com/oauth/.well-known/openid-configuration', |
| client_kwargs={'scope': 'openid profile'} |
| ) |
|
|
| |
| current_batch = b"" |
| last_update_ts = 0 |
| packet_id_counter = 0 |
|
|
| |
| @app.get('/login') |
| async def login(request: Request): |
| redirect_uri = str(request.url_for('auth')).replace("http://", "https://") |
| return await oauth.roblox.authorize_redirect(request, redirect_uri) |
|
|
|
|
| @app.get('/auth') |
| async def auth(request: Request): |
| code = request.query_params.get("code") |
|
|
| if not code: |
| return JSONResponse({"error": "missing code"}, status_code=400) |
|
|
| async with httpx.AsyncClient() as client: |
| token_resp = await client.post( |
| "https://apis.roblox.com/oauth/v1/token", |
| data={ |
| "grant_type": "authorization_code", |
| "code": code, |
| "client_id": CLIENT_ID, |
| "client_secret": CLIENT_SECRET, |
| "redirect_uri": "https://tonyd365-tonys-multifunctional-hall.hf.space/auth" |
| }, |
| headers={ |
| "Content-Type": "application/x-www-form-urlencoded" |
| } |
| ) |
|
|
| token_data = token_resp.json() |
|
|
| access_token = token_data.get("access_token") |
|
|
| if not access_token: |
| return JSONResponse(token_data, status_code=400) |
|
|
| |
| async with httpx.AsyncClient() as client: |
| user_resp = await client.get( |
| "https://apis.roblox.com/oauth/v1/userinfo", |
| headers={"Authorization": f"Bearer {access_token}"} |
| ) |
|
|
| userinfo = user_resp.json() |
|
|
| if not userinfo: |
| return JSONResponse({"error": "userinfo missing"}, status_code=400) |
|
|
| user_data = { |
| 'id': str(userinfo.get('sub')), |
| 'name': userinfo.get('preferred_username', 'Roblox Player'), |
| 'picture': userinfo.get('picture', ''), |
| 'exp': int((datetime.datetime.utcnow() + datetime.timedelta(hours=2)).timestamp()) |
| } |
|
|
| jwt_token = jwt.encode(user_data, JWT_SECRET, algorithm="HS256") |
|
|
| if isinstance(jwt_token, bytes): |
| jwt_token = jwt_token.decode('utf-8') |
|
|
| return RedirectResponse( |
| url=f"https://tonys-virtual-stage-display.pages.dev/?token={jwt_token}" |
| ) |
|
|
| @app.get('/logout') |
| async def logout(request: Request): |
| request.session.pop('user', None) |
| return RedirectResponse(url='https://tonys-virtual-stage-display.pages.dev/') |
|
|
| @app.get('/api/user') |
| async def get_user_status(authorization: str = Header(None)): |
| if not authorization: |
| return JSONResponse({"logged_in": False}) |
|
|
| try: |
| token = authorization.replace("Bearer ", "") |
| user = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) |
|
|
| return JSONResponse({ |
| "logged_in": True, |
| "user": user, |
| "is_whitelisted": True if user["id"] in WHITE_LIST else False |
| }) |
| except: |
| return JSONResponse({"logged_in": False}) |
|
|
| |
| @app.websocket("/ws/producer") |
| async def websocket_endpoint(websocket: WebSocket, token: str = Query(None)): |
| if not token: |
| await websocket.close(code=1008) |
| return |
| try: |
| user = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) |
| except: |
| await websocket.close(code=1008) |
| return |
|
|
| if user['id'] not in WHITE_LIST: |
| await websocket.close(code=1008) |
| return |
|
|
| await websocket.accept() |
| global current_batch, last_update_ts, packet_id_counter |
| try: |
| while True: |
| data = await websocket.receive_bytes() |
| if data: |
| current_batch = data |
| last_update_ts = time.time() |
| packet_id_counter += 1 |
| except WebSocketDisconnect: pass |
|
|
| |
| @app.get("/poll") |
| async def poll(): |
| return Response( |
| content=current_batch, |
| media_type="application/octet-stream", |
| headers={ |
| "X-Timestamp": str(last_update_ts), |
| "X-Packet-ID": str(packet_id_counter), |
| "Cache-Control": "no-cache" |
| } |
| ) |
|
|
| static_path = os.path.join(os.path.dirname(__file__), "static") |
| if not os.path.exists(static_path): os.makedirs(static_path) |
|
|
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
|
|
| @app.get("/") |
| async def blankget(request: Request): |
| return RedirectResponse(url='https://tonys-virtual-stage-display.pages.dev/') |
|
|
| @app.head("/") |
| async def HeadGet(request: Request): |
| return JSONResponse({"status": "OK"}) |
|
|