Add newer CoalGov update
This commit is contained in:
491
property-web/app.py
Normal file
491
property-web/app.py
Normal file
@@ -0,0 +1,491 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from http.cookies import SimpleCookie
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
DB_PATH = Path(os.environ.get("COALGOV_DB", "/opt/mcworldgen-server/data/plugins/CoalGov/coalgov.db"))
|
||||
SECRET_PATH = Path(os.environ.get("COALGOV_WEB_SECRET", str(BASE_DIR / "session.secret")))
|
||||
HOST = os.environ.get("COALGOV_WEB_HOST", "0.0.0.0")
|
||||
PORT = int(os.environ.get("COALGOV_WEB_PORT", "8088"))
|
||||
SESSION_TTL = int(os.environ.get("COALGOV_WEB_SESSION_SECONDS", "43200"))
|
||||
BLUEMAP_URL = os.environ.get("COALGOV_BLUEMAP_URL", "")
|
||||
BLUEMAP_MARKERS_PATH = os.environ.get("COALGOV_BLUEMAP_MARKERS_PATH", "")
|
||||
BLUEMAP_SYNC_SECONDS = int(os.environ.get("COALGOV_BLUEMAP_SYNC_SECONDS", "30"))
|
||||
|
||||
|
||||
def secret():
|
||||
if SECRET_PATH.exists():
|
||||
return SECRET_PATH.read_bytes().strip()
|
||||
value = secrets.token_bytes(32)
|
||||
SECRET_PATH.write_bytes(base64.urlsafe_b64encode(value))
|
||||
os.chmod(SECRET_PATH, 0o600)
|
||||
return SECRET_PATH.read_bytes().strip()
|
||||
|
||||
|
||||
SECRET = secret()
|
||||
|
||||
|
||||
def db():
|
||||
con = sqlite3.connect(DB_PATH)
|
||||
con.row_factory = sqlite3.Row
|
||||
return con
|
||||
|
||||
|
||||
def now_ms():
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def token_hash(token):
|
||||
return hashlib.sha256(token.upper().replace(" ", "").encode()).hexdigest()
|
||||
|
||||
|
||||
def sign(payload):
|
||||
raw = json.dumps(payload, separators=(",", ":")).encode()
|
||||
body = base64.urlsafe_b64encode(raw).decode().rstrip("=")
|
||||
sig = hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
|
||||
return f"{body}.{sig}"
|
||||
|
||||
|
||||
def unsign(value):
|
||||
if not value or "." not in value:
|
||||
return None
|
||||
body, sig = value.rsplit(".", 1)
|
||||
expected = hmac.new(SECRET, body.encode(), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(sig, expected):
|
||||
return None
|
||||
try:
|
||||
raw = base64.urlsafe_b64decode(body + "=" * (-len(body) % 4))
|
||||
payload = json.loads(raw)
|
||||
except (ValueError, json.JSONDecodeError):
|
||||
return None
|
||||
if payload.get("exp", 0) < int(time.time()):
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def money(cents):
|
||||
cents = int(cents or 0)
|
||||
coal, cc = divmod(abs(cents), 100)
|
||||
prefix = "-" if cents < 0 else ""
|
||||
if coal and cc:
|
||||
return f"{prefix}{coal} coal {cc}cc"
|
||||
if coal:
|
||||
return f"{prefix}{coal} coal"
|
||||
return f"{prefix}{cc}cc"
|
||||
|
||||
|
||||
def rows(query, args=()):
|
||||
with db() as con:
|
||||
return [dict(row) for row in con.execute(query, args).fetchall()]
|
||||
|
||||
|
||||
def row(query, args=()):
|
||||
with db() as con:
|
||||
found = con.execute(query, args).fetchone()
|
||||
return dict(found) if found else None
|
||||
|
||||
|
||||
def display_claim(claim):
|
||||
name = claim.get("display_name") or f"Claim #{claim['id']}"
|
||||
return {**claim, "display": name, "tax_due_text": money(claim.get("tax_due", 0))}
|
||||
|
||||
|
||||
def can_manage(con, uuid, claim_id):
|
||||
claim = con.execute("SELECT owner_uuid FROM claims WHERE id = ?", (claim_id,)).fetchone()
|
||||
if not claim:
|
||||
return False
|
||||
if claim["owner_uuid"] == uuid:
|
||||
return True
|
||||
grant = con.execute("""
|
||||
SELECT 1 FROM claim_permissions
|
||||
WHERE claim_id = ? AND player_uuid = ? AND permission = 'MANAGE'
|
||||
LIMIT 1
|
||||
""", (claim_id, uuid)).fetchone()
|
||||
return bool(grant)
|
||||
|
||||
|
||||
def color(kind):
|
||||
colors = {
|
||||
"HOMESTEAD": {"r": 102, "g": 170, "b": 204},
|
||||
"INDUSTRIAL": {"r": 204, "g": 153, "b": 102},
|
||||
"GOVERNMENT": {"r": 221, "g": 102, "b": 102},
|
||||
"PUBLIC": {"r": 119, "g": 204, "b": 119},
|
||||
"PROTECTED_PRESERVE": {"r": 102, "g": 204, "b": 102},
|
||||
"MINING_CONCESSION": {"r": 204, "g": 204, "b": 102},
|
||||
"BORDER_ZONE": {"r": 204, "g": 102, "b": 204},
|
||||
}
|
||||
return colors.get(kind, {"r": 170, "g": 170, "b": 170})
|
||||
|
||||
|
||||
def rect_shape(area):
|
||||
x1, x2 = sorted((int(area["x1"]), int(area["x2"])))
|
||||
z1, z2 = sorted((int(area["z1"]), int(area["z2"])))
|
||||
return [
|
||||
{"x": x1, "z": z1},
|
||||
{"x": x2 + 1, "z": z1},
|
||||
{"x": x2 + 1, "z": z2 + 1},
|
||||
{"x": x1, "z": z2 + 1},
|
||||
]
|
||||
|
||||
|
||||
def center(shape):
|
||||
return {
|
||||
"x": sum(p["x"] for p in shape) / len(shape),
|
||||
"y": 72,
|
||||
"z": sum(p["z"] for p in shape) / len(shape),
|
||||
}
|
||||
|
||||
|
||||
def marker_detail(title, lines):
|
||||
body = "".join(f"<div>{html.escape(str(line))}</div>" for line in lines)
|
||||
return f"<strong>{html.escape(str(title))}</strong>{body}"
|
||||
|
||||
|
||||
def build_bluemap_markers():
|
||||
with db() as con:
|
||||
claims = [display_claim(dict(r)) for r in con.execute("""
|
||||
SELECT c.*, p.name AS owner_name FROM claims c
|
||||
LEFT JOIN players p ON p.uuid = c.owner_uuid
|
||||
WHERE c.world = 'world'
|
||||
ORDER BY c.id
|
||||
""")]
|
||||
vertices = {}
|
||||
for r in con.execute("SELECT claim_id, x, z FROM claim_vertices ORDER BY claim_id, vertex_order"):
|
||||
vertices.setdefault(r["claim_id"], []).append({"x": int(r["x"]), "z": int(r["z"])})
|
||||
lands = [dict(r) for r in con.execute("SELECT * FROM land_regions WHERE world = 'world' ORDER BY name")]
|
||||
|
||||
claim_markers = []
|
||||
for claim in claims:
|
||||
shape = vertices.get(claim["id"]) or rect_shape(claim)
|
||||
c = color(claim["claim_type"])
|
||||
claim_markers.append({
|
||||
"id": f"claim-{claim['id']}",
|
||||
"type": "shape",
|
||||
"map": "overworld",
|
||||
"position": center(shape),
|
||||
"label": claim["display"],
|
||||
"detail": marker_detail(claim["display"], [
|
||||
f"Type: {claim['claim_type']}",
|
||||
f"Owner: {claim.get('owner_name') or claim['owner_uuid']}",
|
||||
f"Tax due: {claim['tax_due_text']}",
|
||||
]),
|
||||
"shape": shape,
|
||||
"shapeY": 72,
|
||||
"depthTest": False,
|
||||
"lineWidth": 3,
|
||||
"lineColor": {**c, "a": 1.0},
|
||||
"fillColor": {**c, "a": 0.28},
|
||||
"minDistance": 10.0,
|
||||
"maxDistance": 10000000.0,
|
||||
})
|
||||
|
||||
land_markers = []
|
||||
for land in lands:
|
||||
shape = rect_shape(land)
|
||||
c = color(land["land_class"])
|
||||
land_markers.append({
|
||||
"id": f"land-{land['id']}",
|
||||
"type": "shape",
|
||||
"map": "overworld",
|
||||
"position": center(shape),
|
||||
"label": land["name"],
|
||||
"detail": marker_detail(land["name"], [f"Class: {land['land_class']}"]),
|
||||
"shape": shape,
|
||||
"shapeY": 73,
|
||||
"depthTest": False,
|
||||
"lineWidth": 2,
|
||||
"lineColor": {**c, "a": 1.0},
|
||||
"fillColor": {**c, "a": 0.16},
|
||||
"minDistance": 10.0,
|
||||
"maxDistance": 10000000.0,
|
||||
})
|
||||
|
||||
return {
|
||||
"coalgov-claims": {
|
||||
"label": "CoalGov Claims",
|
||||
"toggleable": True,
|
||||
"defaultHidden": False,
|
||||
"sorting": 10,
|
||||
"markers": {marker["id"]: {k: v for k, v in marker.items() if k != "id"} for marker in claim_markers},
|
||||
},
|
||||
"coalgov-land": {
|
||||
"label": "CoalGov Land",
|
||||
"toggleable": True,
|
||||
"defaultHidden": False,
|
||||
"sorting": 11,
|
||||
"markers": {marker["id"]: {k: v for k, v in marker.items() if k != "id"} for marker in land_markers},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def sync_bluemap_markers_once():
|
||||
if not BLUEMAP_MARKERS_PATH:
|
||||
return False
|
||||
target = Path(BLUEMAP_MARKERS_PATH)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = target.with_suffix(target.suffix + ".tmp")
|
||||
tmp.write_text(json.dumps(build_bluemap_markers(), separators=(",", ":")))
|
||||
tmp.replace(target)
|
||||
return True
|
||||
|
||||
|
||||
def sync_bluemap_markers_loop():
|
||||
while True:
|
||||
try:
|
||||
sync_bluemap_markers_once()
|
||||
except Exception as exc:
|
||||
print(f"BlueMap marker sync failed: {exc}", flush=True)
|
||||
time.sleep(max(5, BLUEMAP_SYNC_SECONDS))
|
||||
|
||||
|
||||
INDEX = """<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>CoalGov Property Map</title>
|
||||
<style>
|
||||
body { margin: 0; font-family: system-ui, sans-serif; background: #111; color: #eee; }
|
||||
header { display: flex; gap: 12px; align-items: center; padding: 10px 14px; background: #202020; border-bottom: 1px solid #333; }
|
||||
h1 { font-size: 18px; margin: 0; }
|
||||
main { display: grid; grid-template-columns: 1fr 360px; height: calc(100vh - 48px); }
|
||||
#mapwrap { position: relative; min-width: 0; background: #151515; }
|
||||
#bluemap { width: 100%; height: 100%; border: 0; display: block; }
|
||||
aside { overflow: auto; padding: 14px; background: #1b1b1b; border-left: 1px solid #333; }
|
||||
input, button, select { background: #272727; color: #eee; border: 1px solid #444; border-radius: 4px; padding: 8px; }
|
||||
button { cursor: pointer; }
|
||||
.row { display: flex; gap: 8px; margin: 8px 0; }
|
||||
.card { border: 1px solid #333; border-radius: 6px; padding: 10px; margin: 10px 0; background: #202020; }
|
||||
.muted { color: #aaa; font-size: 13px; }
|
||||
.hidden { display: none; }
|
||||
@media (max-width: 900px) { main { grid-template-columns: 1fr; grid-template-rows: 55vh auto; } aside { border-left: 0; border-top: 1px solid #333; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header><h1>CoalGov Property Map</h1><span id="who" class="muted"></span></header>
|
||||
<main>
|
||||
<section id="mapwrap">
|
||||
<iframe id="bluemap"></iframe>
|
||||
</section>
|
||||
<aside>
|
||||
<section id="login">
|
||||
<h2>Token Login</h2>
|
||||
<p class="muted">Run <code>/coalgov webtoken</code> in game, then enter the single-use token here.</p>
|
||||
<div class="row"><input id="token" placeholder="ABCD-1234-EFGH"><button onclick="login()">Log in</button></div>
|
||||
</section>
|
||||
<section id="panel" class="hidden">
|
||||
<div class="row"><button onclick="loadAll()">Refresh</button><button onclick="syncBlueMap()">Sync BlueMap</button><button onclick="logout()">Log out</button></div>
|
||||
<p class="muted">The main pane is BlueMap. CoalGov property and land overlays are published into BlueMap's marker layer.</p>
|
||||
<h2>Your Claims</h2><div id="claims"></div>
|
||||
<h2>Land Notices</h2><div id="notices"></div>
|
||||
</section>
|
||||
</aside>
|
||||
</main>
|
||||
<script>
|
||||
const configuredBlueMapUrl = "__BLUEMAP_URL__";
|
||||
bluemap.src = configuredBlueMapUrl || `${location.protocol}//${location.hostname}:8100/`;
|
||||
let data = {claims: [], lands: []};
|
||||
async function api(path, opts={}) {
|
||||
const r = await fetch(path, {headers: {"Content-Type":"application/json"}, ...opts});
|
||||
if (!r.ok) throw new Error((await r.json()).error || r.statusText);
|
||||
return r.json();
|
||||
}
|
||||
async function login() {
|
||||
try { await api("/api/login", {method:"POST", body:JSON.stringify({token:document.getElementById("token").value})}); await loadAll(); }
|
||||
catch(e) { alert(e.message); }
|
||||
}
|
||||
async function logout() { await api("/api/logout", {method:"POST"}); location.reload(); }
|
||||
async function syncBlueMap() { try { await api("/api/bluemap/sync", {method:"POST"}); const frame = document.getElementById("bluemap"); frame.src = frame.src; } catch(e) { alert(e.message); } }
|
||||
async function loadAll() {
|
||||
const me = await api("/api/me");
|
||||
document.getElementById("who").textContent = me.name + " (" + me.uuid + ")";
|
||||
document.getElementById("login").classList.add("hidden");
|
||||
document.getElementById("panel").classList.remove("hidden");
|
||||
data = await api("/api/map");
|
||||
renderClaims();
|
||||
}
|
||||
function renderClaims() {
|
||||
claims.innerHTML = "";
|
||||
for (const c of data.claims.filter(c=>c.manageable)) {
|
||||
const div = document.createElement("div"); div.className = "card";
|
||||
div.innerHTML = `<b>${c.display}</b><div class="muted">${c.claim_type} ${c.world} X ${c.x1}..${c.x2} Z ${c.z1}..${c.z2}<br>Owner: ${c.owner_name || c.owner_uuid}<br>Tax due: ${c.tax_due_text}</div>
|
||||
<div class="row"><input value="${c.display}" id="n${c.id}"><button onclick="renameClaim(${c.id})">Rename</button></div>
|
||||
<div class="row"><input placeholder="target player name or UUID" id="t${c.id}"><button onclick="transferClaim(${c.id})">Transfer</button></div>
|
||||
<button onclick="payTax(${c.id})">Pay Claim Tax</button>`;
|
||||
claims.appendChild(div);
|
||||
}
|
||||
notices.innerHTML = data.lands.map(l => `<div class="card"><b>${l.name}</b><div class="muted">${l.land_class} ${l.world} X ${l.x1}..${l.x2} Z ${l.z1}..${l.z2}</div></div>`).join("");
|
||||
}
|
||||
async function renameClaim(id) { try { await api(`/api/claims/${id}/rename`, {method:"POST", body:JSON.stringify({name:document.getElementById("n"+id).value})}); await loadAll(); } catch(e) { alert(e.message); } }
|
||||
async function transferClaim(id) { try { await api(`/api/claims/${id}/transfer`, {method:"POST", body:JSON.stringify({target:document.getElementById("t"+id).value})}); await loadAll(); } catch(e) { alert(e.message); } }
|
||||
async function payTax(id) { try { const r = await api(`/api/claims/${id}/pay-tax`, {method:"POST"}); alert(r.message); await loadAll(); } catch(e) { alert(e.message); } }
|
||||
loadAll().catch(()=>{});
|
||||
</script>
|
||||
</body></html>""".replace("__BLUEMAP_URL__", html.escape(BLUEMAP_URL, quote=True))
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
if path == "/":
|
||||
return self.html(INDEX)
|
||||
if path == "/api/me":
|
||||
user = self.require_user()
|
||||
if user:
|
||||
player = row("SELECT name FROM players WHERE uuid = ?", (user["uuid"],)) or {}
|
||||
self.json({"uuid": user["uuid"], "name": player.get("name", user["uuid"])})
|
||||
return
|
||||
if path == "/api/map":
|
||||
user = self.require_user()
|
||||
if not user:
|
||||
return
|
||||
uuid = user["uuid"]
|
||||
with db() as con:
|
||||
claims = [display_claim(dict(r)) for r in con.execute("""
|
||||
SELECT c.*, p.name AS owner_name FROM claims c
|
||||
LEFT JOIN players p ON p.uuid = c.owner_uuid
|
||||
ORDER BY c.id
|
||||
""")]
|
||||
vertices = {}
|
||||
for r in con.execute("SELECT claim_id, x, z FROM claim_vertices ORDER BY claim_id, vertex_order"):
|
||||
vertices.setdefault(r["claim_id"], []).append({"x": r["x"], "z": r["z"]})
|
||||
for c in claims:
|
||||
c["vertices"] = vertices.get(c["id"], [])
|
||||
c["manageable"] = c["owner_uuid"] == uuid or bool(con.execute("""
|
||||
SELECT 1 FROM claim_permissions WHERE claim_id = ? AND player_uuid = ? AND permission = 'MANAGE'
|
||||
""", (c["id"], uuid)).fetchone())
|
||||
lands = [dict(r) for r in con.execute("SELECT * FROM land_regions ORDER BY name")]
|
||||
self.json({"claims": claims, "lands": lands})
|
||||
return
|
||||
self.error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
|
||||
def do_POST(self):
|
||||
path = urlparse(self.path).path
|
||||
if path == "/api/login":
|
||||
body = self.body()
|
||||
token = body.get("token", "")
|
||||
hashed = token_hash(token)
|
||||
with db() as con:
|
||||
found = con.execute("""
|
||||
SELECT token_hash, player_uuid FROM web_tokens
|
||||
WHERE token_hash = ? AND expires_at > ? AND used_at IS NULL
|
||||
""", (hashed, now_ms())).fetchone()
|
||||
if not found:
|
||||
return self.error(HTTPStatus.UNAUTHORIZED, "Invalid or expired token")
|
||||
con.execute("UPDATE web_tokens SET used_at = ? WHERE token_hash = ?", (now_ms(), hashed))
|
||||
con.commit()
|
||||
cookie = f"cg_session={sign({'uuid': found['player_uuid'], 'exp': int(time.time()) + SESSION_TTL})}; HttpOnly; SameSite=Lax; Path=/"
|
||||
self.json({"ok": True}, headers={"Set-Cookie": cookie})
|
||||
return
|
||||
if path == "/api/logout":
|
||||
self.json({"ok": True}, headers={"Set-Cookie": "cg_session=; Max-Age=0; Path=/"})
|
||||
return
|
||||
user = self.require_user()
|
||||
if not user:
|
||||
return
|
||||
if path == "/api/bluemap/sync":
|
||||
if sync_bluemap_markers_once():
|
||||
return self.json({"ok": True})
|
||||
return self.error(HTTPStatus.BAD_REQUEST, "BlueMap marker sync is not configured")
|
||||
if path.startswith("/api/claims/") and path.endswith("/rename"):
|
||||
claim_id = int(path.split("/")[3])
|
||||
name = str(self.body().get("name", "")).strip()[:48]
|
||||
if not name:
|
||||
return self.error(HTTPStatus.BAD_REQUEST, "Name required")
|
||||
with db() as con:
|
||||
if not can_manage(con, user["uuid"], claim_id):
|
||||
return self.error(HTTPStatus.FORBIDDEN, "You cannot manage that claim")
|
||||
con.execute("UPDATE claims SET display_name = ? WHERE id = ?", (name, claim_id))
|
||||
con.commit()
|
||||
return self.json({"ok": True})
|
||||
if path.startswith("/api/claims/") and path.endswith("/transfer"):
|
||||
claim_id = int(path.split("/")[3])
|
||||
target = str(self.body().get("target", "")).strip()
|
||||
with db() as con:
|
||||
if not can_manage(con, user["uuid"], claim_id):
|
||||
return self.error(HTTPStatus.FORBIDDEN, "You cannot manage that claim")
|
||||
player = con.execute("SELECT uuid, name FROM players WHERE uuid = ? OR lower(name) = lower(?)", (target, target)).fetchone()
|
||||
if not player:
|
||||
return self.error(HTTPStatus.BAD_REQUEST, "Target player not found in CoalGov")
|
||||
con.execute("UPDATE claims SET owner_uuid = ? WHERE id = ?", (player["uuid"], claim_id))
|
||||
con.execute("DELETE FROM claim_permissions WHERE claim_id = ?", (claim_id,))
|
||||
con.commit()
|
||||
return self.json({"ok": True})
|
||||
if path.startswith("/api/claims/") and path.endswith("/pay-tax"):
|
||||
claim_id = int(path.split("/")[3])
|
||||
with db() as con:
|
||||
if not can_manage(con, user["uuid"], claim_id):
|
||||
return self.error(HTTPStatus.FORBIDDEN, "You cannot manage that claim")
|
||||
claim = con.execute("SELECT tax_due FROM claims WHERE id = ?", (claim_id,)).fetchone()
|
||||
due = int(claim["tax_due"] or 0) if claim else 0
|
||||
if due <= 0:
|
||||
return self.json({"ok": True, "message": "No tax due."})
|
||||
updated = con.execute("UPDATE players SET coal_balance = coal_balance - ? WHERE uuid = ? AND coal_balance >= ?", (due, user["uuid"], due)).rowcount
|
||||
if updated != 1:
|
||||
return self.error(HTTPStatus.BAD_REQUEST, f"Balance too low. Need {money(due)}.")
|
||||
con.execute("UPDATE claims SET tax_due = 0 WHERE id = ?", (claim_id,))
|
||||
con.execute("INSERT INTO transactions(from_uuid,to_uuid,amount,reason,created_at) VALUES (?,?,?,?,?)", (user["uuid"], None, due, "claim_tax_web", now_ms()))
|
||||
con.execute("INSERT INTO treasury(id,balance) VALUES ('main', ?) ON CONFLICT(id) DO UPDATE SET balance = balance + excluded.balance", (due,))
|
||||
con.commit()
|
||||
return self.json({"ok": True, "message": f"Paid {money(due)}."})
|
||||
self.error(HTTPStatus.NOT_FOUND, "Not found")
|
||||
|
||||
def require_user(self):
|
||||
cookie = SimpleCookie(self.headers.get("Cookie", ""))
|
||||
payload = unsign(cookie.get("cg_session").value if cookie.get("cg_session") else "")
|
||||
if not payload:
|
||||
self.error(HTTPStatus.UNAUTHORIZED, "Login required")
|
||||
return None
|
||||
return payload
|
||||
|
||||
def body(self):
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
if length <= 0:
|
||||
return {}
|
||||
return json.loads(self.rfile.read(length) or b"{}")
|
||||
|
||||
def html(self, content):
|
||||
raw = content.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def json(self, value, headers=None):
|
||||
raw = json.dumps(value).encode()
|
||||
self.send_response(200)
|
||||
for key, val in (headers or {}).items():
|
||||
self.send_header(key, val)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
def error(self, status, message):
|
||||
raw = json.dumps({"error": message}).encode()
|
||||
self.send_response(status.value)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"CoalGov property web listening on {HOST}:{PORT}, db={DB_PATH}")
|
||||
if BLUEMAP_MARKERS_PATH:
|
||||
threading.Thread(target=sync_bluemap_markers_loop, daemon=True).start()
|
||||
ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()
|
||||
Reference in New Issue
Block a user