TonyD365's picture
Update main.py
31b830b
Raw
History Blame Contribute Delete
5.85 kB
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()
# 环境变量 (必须在 HF Space Settings 中配置)
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})
# --- 推流 WebSocket ---
@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
# --- Roblox 拉取接口 ---
@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("/privacy")
#async def privacy():
# return FileResponse(os.path.join(static_path,"privacy.html"))
#@app.get("/tos")
#async def privacy():
# return FileResponse(os.path.join(static_path,"tos.html"))
#app.mount("/", StaticFiles(directory=static_path, html=True), name="static")
@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"})