update
This commit is contained in:
0
api/app/__init__.py
Normal file
0
api/app/__init__.py
Normal file
BIN
api/app/__pycache__/auth.cpython-312.pyc
Normal file
BIN
api/app/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
api/app/__pycache__/main.cpython-312.pyc
Normal file
BIN
api/app/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
50
api/app/auth.py
Normal file
50
api/app/auth.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import sqlite3
|
||||
import os
|
||||
from pathlib import Path
|
||||
from fastapi import HTTPException, Header
|
||||
|
||||
DB_PATH = os.getenv("DB_PATH", "/data/api_keys.db")
|
||||
SEED_API_KEY = os.getenv("SEED_API_KEY")
|
||||
SEED_GAME = os.getenv("SEED_GAME")
|
||||
|
||||
def get_db():
|
||||
# Ensure the directory exists
|
||||
Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def init_db():
|
||||
conn = get_db()
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
key TEXT PRIMARY KEY,
|
||||
game TEXT NOT NULL,
|
||||
active BOOLEAN DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
# Insert seed key if provided via env
|
||||
if SEED_API_KEY and SEED_GAME:
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO api_keys(key, game) VALUES (?, ?)",
|
||||
(SEED_API_KEY, SEED_GAME)
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
conn.close()
|
||||
|
||||
async def verify_api_key(x_api_key: str = Header(None)) -> str:
|
||||
if not x_api_key:
|
||||
raise HTTPException(status_code=401, detail="X-API-Key header missing")
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT game FROM api_keys WHERE key = ? AND active = 1",
|
||||
(x_api_key,)
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
raise HTTPException(status_code=403, detail="Invalid or inactive API key")
|
||||
return row["game"]
|
||||
17
api/app/enrichment.py
Normal file
17
api/app/enrichment.py
Normal file
@@ -0,0 +1,17 @@
|
||||
def classify_price(robux: int) -> str:
|
||||
if robux <= 99:
|
||||
return "low"
|
||||
elif robux <= 499:
|
||||
return "medium"
|
||||
else:
|
||||
return "high"
|
||||
|
||||
def enrich_event(event, game: str):
|
||||
"""Adds game tag and derived fields."""
|
||||
event.data = event.data or {}
|
||||
# Inject game name into event data as a tag
|
||||
event.data["game"] = game
|
||||
# Derive price group for robux purchases
|
||||
if event.type == "robux_purchase" and "robux" in event.data:
|
||||
event.data["priceGroup"] = classify_price(event.data["robux"])
|
||||
return event
|
||||
46
api/app/influx.py
Normal file
46
api/app/influx.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import os
|
||||
import logging
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
logger = logging.getLogger("influx")
|
||||
|
||||
INFLUX_URL = os.getenv("INFLUX_URL", "")
|
||||
INFLUX_TOKEN = os.getenv("INFLUX_TOKEN", "")
|
||||
INFLUX_ORG = os.getenv("INFLUX_ORG", "")
|
||||
INFLUX_BUCKET = os.getenv("INFLUX_BUCKET", "")
|
||||
|
||||
client = None
|
||||
write_api = None
|
||||
|
||||
if INFLUX_URL and INFLUX_TOKEN:
|
||||
try:
|
||||
client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)
|
||||
write_api = client.write_api(write_options=SYNCHRONOUS)
|
||||
logger.info("InfluxDB client initialised")
|
||||
except Exception as e:
|
||||
logger.error(f"InfluxDB client creation failed: {e}")
|
||||
client = None
|
||||
else:
|
||||
logger.warning("InfluxDB environment variables missing – writing disabled")
|
||||
|
||||
def write_event(event) -> bool:
|
||||
if write_api is None:
|
||||
logger.debug("InfluxDB not available, skipping write")
|
||||
return False
|
||||
try:
|
||||
p = Point(event.type).time(event.time * 1_000_000_000)
|
||||
p.tag("game", event.data.get("game", "unknown"))
|
||||
p.tag("serverId", event.serverId)
|
||||
# Optional tags
|
||||
if "oreType" in event.data:
|
||||
p.tag("oreType", event.data["oreType"])
|
||||
# Write all numeric/string fields
|
||||
for k, v in event.data.items():
|
||||
if isinstance(v, (int, float, str, bool)):
|
||||
p.field(k, v)
|
||||
write_api.write(bucket=INFLUX_BUCKET, record=p)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Write failed: {e}")
|
||||
return False
|
||||
@@ -1,7 +1,26 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from .auth import init_db, verify_api_key
|
||||
from .models import Event, BatchEvents
|
||||
from .enrichment import enrich_event
|
||||
from .influx import write_event
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
init_db()
|
||||
|
||||
app = FastAPI(title="Signal - Roblox Telemetry")
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.post("/api/log")
|
||||
async def ingest_event(payload: Event | BatchEvents, game: str = Depends(verify_api_key)):
|
||||
if isinstance(payload, BatchEvents):
|
||||
for event in payload.events:
|
||||
enriched = enrich_event(event, game)
|
||||
write_event(enriched)
|
||||
else:
|
||||
enriched = enrich_event(payload, game)
|
||||
write_event(enriched)
|
||||
return {"success": True}
|
||||
11
api/app/models.py
Normal file
11
api/app/models.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, List, Optional
|
||||
|
||||
class Event(BaseModel):
|
||||
type: str
|
||||
time: int # unix timestamp
|
||||
serverId: str
|
||||
data: Optional[dict] = {}
|
||||
|
||||
class BatchEvents(BaseModel):
|
||||
events: List[Event]
|
||||
Reference in New Issue
Block a user