diff --git a/api_server.py b/api_server.py new file mode 100644 index 0000000..66d2e93 --- /dev/null +++ b/api_server.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Nerdhalla API server — serves POI data and server status.""" +import json +import sqlite3 +import subprocess +import sys +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs + +DB_PATH = "/tmp/valheim_poi.db" +NERDCADE_SSH = "aelith@100.86.206.39" +NERDCADE_PORT = 46129 + +class APIHandler(BaseHTTPRequestHandler): + def do_GET(self): + parsed = urlparse(self.path) + path = parsed.path + params = parse_qs(parsed.query) + + if path == "/api/status": + self._handle_status() + elif path == "/api/pois": + self._handle_pois(params) + else: + self.send_error(404, "Not found") + + def _handle_status(self): + try: + result = subprocess.run( + ["ssh", "-p", str(NERDCADE_PORT), "-o", "ConnectTimeout=5", + "-o", "StrictHostKeyChecking=no", NERDCADE_SSH, + "systemctl --user is-active amp* 2>/dev/null || echo 'inactive'"], + capture_output=True, text=True, timeout=10 + ) + online = "active" in result.stdout.lower() + data = {"online": online, "players": None} + except Exception: + data = {"online": False, "players": None} + + self._json_response(data) + + def _handle_pois(self, params): + try: + limit = min(int(params.get("limit", [500])[0]), 11309) + offset = int(params.get("offset", [0])[0]) + biome = params.get("biome", [None])[0] + search = params.get("search", [None])[0] + + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + cur = conn.cursor() + + where_clauses = [] + bindings = [] + if biome: + where_clauses.append("category = ?") + bindings.append(biome) + if search: + where_clauses.append("prefab_name LIKE ?") + bindings.append(f"%{search}%") + + where = "" + if where_clauses: + where = "WHERE " + " AND ".join(where_clauses) + + cur.execute(f"SELECT pos_x, pos_y, pos_z, prefab_name, category FROM locations {where} ORDER BY prefab_name LIMIT ? OFFSET ?", bindings + [limit, offset]) + rows = [{ + "x": r["pos_x"], + "y": r["pos_z"], # Valheim uses Z as north-south + "prefab": r["prefab_name"], + "category": r["category"] + } for r in cur.fetchall()] + conn.close() + + self._json_response(rows) + except Exception as e: + self._json_response({"error": str(e)}, status=500) + + def _json_response(self, data, status=200): + body = json.dumps(data).encode() + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format, *args): + sys.stderr.write("[API] %s - %s\n" % (self.client_address[0], format % args)) + +if __name__ == "__main__": + port = 8081 + server = HTTPServer(("127.0.0.1", port), APIHandler) + print(f"[Nerdhalla API] Listening on 127.0.0.1:{port}") + server.serve_forever() diff --git a/nerdhalla.conf b/nerdhalla.conf new file mode 100644 index 0000000..735437a --- /dev/null +++ b/nerdhalla.conf @@ -0,0 +1,21 @@ +nerdhalla.nerdcade.cc { + root * /var/www/nerdhalla + file_server + + # API reverse proxy + handle_path /api/* { + reverse_proxy 127.0.0.1:8081 + } + + # Security headers + header { + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + } + + # Logs + log { + output file /var/log/caddy/nerdhalla.log + } +}