Files
rdv/pyapi/main.py
2026-01-03 13:21:39 +02:00

518 lines
18 KiB
Python

import json
import logging
import os
import time
from typing import Dict, List, Optional
from urllib.parse import urlencode, quote
import aiohttp
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse, Response
from pydantic import BaseModel
load_dotenv()
# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
app = FastAPI()
logger.info("FastAPI Proxy Server initialized")
SCRAPINGBEE_API_KEY = os.getenv("SCRAPINGBEE_API_KEY")
if not SCRAPINGBEE_API_KEY:
raise ValueError("SCRAPINGBEE_API_KEY is not set")
CONSTANTS = {
"SESSION_KEY_NAME": "SID",
"SESSION_EXPIRE_TIME_MS": 6 * 60 * 60 * 1000,
"POST_SESSION_KEY": "postsession",
"LAST_FETCHED_KEY": "LAST_FETCHED",
"SCRAP_API_URL": "https://gamebooking24.com/lottery-api",
"SCRAP_API_SESSION_KEY": "SRAJWT",
"SCRAPINGBEE_BASE_URL": "https://app.scrapingbee.com/api/v1",
"SCRAP_API_BASE_HEADERS": {
"Host": "gamebooking24.com",
"Sec-Ch-Ua": '"Not/A)Brand";v="8", "Chromium";v="126"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Fetch-Site": "cross-site",
"Sec-Fetch-Mode": "no-cors",
"Sec-Fetch-Dest": "image",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.9",
"Access-Control-Allow-Origin": "*",
"Accept": "application/json, text/plain, */*",
"Origin": "https://gamebooking24.com",
"Referer": "https://gamebooking24.com/",
"Priority": "u=1, i",
},
}
# Middleware for logging all requests
@app.middleware("http")
async def log_requests(request: Request, call_next):
start_time = time.time()
# Log incoming request
logger.info(f"{request.method} {request.url.path}")
if request.query_params:
logger.debug(f" Query params: {dict(request.query_params)}")
# Process request
response = await call_next(request)
# Log response
duration = (time.time() - start_time) * 1000
logger.info(
f"{request.method} {request.url.path} [{response.status_code}] ({duration:.2f}ms)"
)
return response
def build_headers(
authorization: Optional[str] = None, extra_headers: Optional[Dict[str, str]] = None
) -> Dict[str, str]:
"""Build headers dict for requests"""
headers = CONSTANTS["SCRAP_API_BASE_HEADERS"].copy()
if authorization:
headers["Authorization"] = authorization
if extra_headers:
headers.update(extra_headers)
return headers
async def make_get_request(
url: str, params: Optional[Dict] = None, headers: Optional[Dict] = None
):
"""Make a GET request using ScrapingBee"""
# Add query params to the target URL if provided
if params:
url_with_params = f"{url}?{urlencode(params)}"
else:
url_with_params = url
logger.debug(f"[ScrapingBee GET] Target URL: {url_with_params}")
# Build the ScrapingBee request params
# Note: aiohttp will automatically URL-encode the params, including the 'url' value
scrapingbee_params = {
"api_key": SCRAPINGBEE_API_KEY,
"url": url_with_params,
"render_js": "true",
"block_resources": "false",
"transparent_status_code": "true", # Pass through the actual status code from target site
}
# Forward headers to target site if provided (for Authorization, etc.)
if headers and "Authorization" in headers:
scrapingbee_params["forward_headers"] = "true"
# Make the request to ScrapingBee using aiohttp
# Note: Don't pass custom headers to ScrapingBee - they're for the target site
# If needed, use ScrapingBee's forward_headers parameter instead
async with aiohttp.ClientSession() as session:
async with session.get(
CONSTANTS["SCRAPINGBEE_BASE_URL"],
params=scrapingbee_params,
timeout=aiohttp.ClientTimeout(total=60),
) as response:
# Read content before context manager exits
content = await response.read()
# Log error responses for debugging
if response.status != 200:
try:
error_text = content.decode('utf-8')[:500]
logger.error(f"[ScrapingBee GET] Status {response.status}, Response: {error_text}")
except:
logger.error(f"[ScrapingBee GET] Status {response.status}, Response (non-text): {len(content)} bytes")
# Create a simple response object with the data
class SimpleResponse:
def __init__(self, status, headers, content_bytes):
self.status_code = status
self.headers = headers
self._content = content_bytes
self._text = None
self._json = None
async def text(self):
if self._text is None:
self._text = self._content.decode('utf-8')
return self._text
async def json(self):
if self._json is None:
self._json = json.loads(await self.text())
return self._json
async def content(self):
return self._content
return SimpleResponse(response.status, response.headers, content)
async def make_post_request(url: str, data: dict, headers: Optional[Dict] = None):
"""Make a POST request using ScrapingBee"""
# Build the ScrapingBee request params
scrapingbee_params = {
"api_key": SCRAPINGBEE_API_KEY,
"url": url,
"render_js": "true",
"block_resources": "false",
}
# ScrapingBee POST requests: pass JSON body as a parameter
scrapingbee_params["body"] = json.dumps(data)
# Forward headers to target site if provided
# Note: ScrapingBee's forward_headers forwards common headers automatically
# For custom headers like Authorization, we may need to use cookies parameter
if headers and "Authorization" in headers:
scrapingbee_params["forward_headers"] = "true"
# TODO: May need to pass Authorization via cookies if forward_headers doesn't work
# Make the POST request to ScrapingBee using aiohttp
# ScrapingBee HTML API uses GET even for POST requests - the body is passed as a param
async with aiohttp.ClientSession() as session:
async with session.get(
CONSTANTS["SCRAPINGBEE_BASE_URL"],
params=scrapingbee_params,
timeout=aiohttp.ClientTimeout(total=60),
) as response:
# Read content before context manager exits
content = await response.read()
# Create a simple response object with the data
class SimpleResponse:
def __init__(self, status, headers, content_bytes):
self.status_code = status
self.headers = headers
self._content = content_bytes
self._text = None
self._json = None
async def text(self):
if self._text is None:
self._text = self._content.decode('utf-8')
return self._text
async def json(self):
if self._json is None:
self._json = json.loads(await self.text())
return self._json
async def content(self):
return self._content
return SimpleResponse(response.status, response.headers, content)
# Pydantic models for request bodies
class LoginPayload(BaseModel):
userId: str
password: str
verifyToken: str
code: str
userType: int
class DealerListPayload(BaseModel):
page: int
pageSize: int
parentDistributor: int
class DistributorListPayload(BaseModel):
page: int
pageSize: int
parentDistributor: int
class BookListPayload(BaseModel):
userType: int
userIds: List[int]
drawId: int
startDate: str
endDate: str
beAdmin: bool
containImported: bool
keyword: str
class AddMultiplePayload(BaseModel):
dealerId: int
drawId: int
closeTime: str
date: str
changedBalance: int
insertData: str
class DeleteMultiplePayload(BaseModel):
bookIds: List[str]
closeTime: str
dealerId: int
drawId: int
@app.get("/ping")
def ping():
logger.info("Ping request received")
return {"status": "pong"}
@app.get("/v1/user/get-balance")
async def get_balance(userId: int, authorization: str):
logger.info(f"[GET /v1/user/get-balance] userId={userId}")
try:
headers = build_headers(authorization=authorization)
response = await make_get_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/user/get-balance",
params={"userId": userId},
headers=headers,
)
logger.info(f"[GET /v1/user/get-balance] Response: {response.status_code}")
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[GET /v1/user/get-balance] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/v1/auth/login")
async def login(payload: LoginPayload):
logger.info(f"[POST /v1/auth/login] - payload={payload.model_dump()}")
try:
headers = build_headers(extra_headers={"Content-Type": "application/json"})
response = await make_post_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/auth/login",
data=payload.model_dump(),
headers=headers,
)
logger.info(f"[POST /v1/auth/login] Response: {response.status_code}")
# Handle non-JSON responses (e.g., 403 HTML pages)
if response.status_code == 403:
response_text = await response.text()
logger.error(
f"[POST /v1/auth/login] 403 Forbidden - Response: {response_text[:500]}"
)
raise HTTPException(status_code=403, detail="Request blocked")
# Try to parse as JSON
try:
response_json = await response.json()
except Exception as json_error:
response_text = await response.text()
logger.error(
f"[POST /v1/auth/login] Failed to parse JSON response: {json_error}"
)
logger.error(f"[POST /v1/auth/login] Response text: {response_text[:500]}")
raise HTTPException(
status_code=500, detail=f"Invalid JSON response: {str(json_error)}"
)
return JSONResponse(content=response_json, status_code=response.status_code)
except HTTPException:
raise
except Exception as e:
logger.error(f"[POST /v1/auth/login] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/verify/image")
async def get_captcha(uuid: str):
logger.info(f"[GET /verify/image] uuid={uuid}")
try:
headers = build_headers(
extra_headers={
"Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
}
)
response = await make_get_request(
f"{CONSTANTS['SCRAP_API_URL']}/verify/image",
params={"uuid": uuid},
headers=headers,
)
if response.status_code == 403:
logger.error("[GET /verify/image] 403 Forbidden - Request blocked")
logger.error(
f"[GET /verify/image] Response headers: {dict(response.headers)}"
)
response_text = await response.text()
logger.error(f"[GET /verify/image] Response text: {response_text[:500]}")
content = await response.content()
logger.info(
f"[GET /verify/image] Response: {response.status_code}, size={len(content)} bytes"
)
return Response(
content=content,
media_type="image/png",
status_code=response.status_code,
)
except Exception as e:
logger.error(f"[GET /verify/image] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/v1/user/dealer-list")
async def dealer_list(payload: DealerListPayload, authorization: str):
logger.info(
f"[POST /v1/user/dealer-list] parentDistributor={payload.parentDistributor}, page={payload.page}, pageSize={payload.pageSize}"
)
try:
headers = build_headers(
authorization=authorization,
extra_headers={"Content-Type": "application/json"},
)
response = await make_post_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/user/dealer-list",
data=payload.model_dump(),
headers=headers,
)
logger.info(f"[POST /v1/user/dealer-list] Response: {response.status_code}")
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[POST /v1/user/dealer-list] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/v1/user/distributor-list")
async def distributor_list(payload: DistributorListPayload, authorization: str):
logger.info(
f"[POST /v1/user/distributor-list] parentDistributor={payload.parentDistributor}, page={payload.page}, pageSize={payload.pageSize}"
)
try:
headers = build_headers(
authorization=authorization,
extra_headers={"Content-Type": "application/json"},
)
response = await make_post_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/user/distributor-list",
data=payload.model_dump(),
headers=headers,
)
logger.info(
f"[POST /v1/user/distributor-list] Response: {response.status_code}"
)
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[POST /v1/user/distributor-list] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/v1/draw/list-my")
async def list_draws(userId: int, authorization: str):
logger.info(f"[GET /v1/draw/list-my] userId={userId}")
try:
headers = build_headers(
authorization=authorization,
extra_headers={"Content-Type": "application/json"},
)
response = await make_get_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/draw/list-my",
params={"userId": userId},
headers=headers,
)
logger.info(f"[GET /v1/draw/list-my] Response: {response.status_code}")
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[GET /v1/draw/list-my] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/v1/book/list2")
async def book_list(payload: BookListPayload, authorization: str):
logger.info(
f"[POST /v1/book/list2] drawId={payload.drawId}, userIds={len(payload.userIds)}, date={payload.startDate}"
)
try:
headers = build_headers(
authorization=authorization,
extra_headers={"Content-Type": "application/json"},
)
response = await make_post_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/book/list2",
data=payload.model_dump(),
headers=headers,
)
logger.info(f"[POST /v1/book/list2] Response: {response.status_code}")
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[POST /v1/book/list2] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/v1/book/add-multiple")
async def add_multiple(payload: AddMultiplePayload, authorization: str):
entries_count = len(payload.insertData.split(";")) if payload.insertData else 0
logger.info(
f"[POST /v1/book/add-multiple] dealerId={payload.dealerId}, drawId={payload.drawId}, entries={entries_count}, balance={payload.changedBalance}"
)
try:
headers = build_headers(
authorization=authorization,
extra_headers={"Content-Type": "application/json;charset=UTF-8"},
)
response = await make_post_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/book/add-multiple",
data=payload.model_dump(),
headers=headers,
)
logger.info(f"[POST /v1/book/add-multiple] Response: {response.status_code}")
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[POST /v1/book/add-multiple] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
@app.post("/v1/book/delete-multiple")
async def delete_multiple(payload: DeleteMultiplePayload, authorization: str):
logger.info(
f"[POST /v1/book/delete-multiple] dealerId={payload.dealerId}, drawId={payload.drawId}, bookIds={len(payload.bookIds)}"
)
try:
headers = build_headers(
authorization=authorization,
extra_headers={"Content-Type": "application/json;charset=UTF-8"},
)
response = await make_post_request(
f"{CONSTANTS['SCRAP_API_URL']}/v1/book/delete-multiple",
data=payload.model_dump(),
headers=headers,
)
logger.info(f"[POST /v1/book/delete-multiple] Response: {response.status_code}")
return JSONResponse(
content=await response.json(), status_code=response.status_code
)
except Exception as e:
logger.error(f"[POST /v1/book/delete-multiple] Error: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))