bzl

self-hosted ephemeral community engine
Log | Files | Refs | README | LICENSE

server.js (5110B)


      1 const fs = require("fs");
      2 const path = require("path");
      3 const http = require("http");
      4 const https = require("https");
      5 
      6 const CONFIG_PATH = path.join(__dirname, "config.json");
      7 
      8 function readJson(filePath, fallback) {
      9   try {
     10     return JSON.parse(fs.readFileSync(filePath, "utf8"));
     11   } catch {
     12     return fallback;
     13   }
     14 }
     15 
     16 function writeJson(filePath, value) {
     17   fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
     18 }
     19 
     20 function normalizeUrl(s) {
     21   const raw = String(s || "").trim();
     22   if (!raw) return "";
     23   try {
     24     const u = new URL(raw);
     25     if (u.protocol !== "http:" && u.protocol !== "https:") return "";
     26     u.hash = "";
     27     return u.toString().replace(/\/+$/, "");
     28   } catch {
     29     return "";
     30   }
     31 }
     32 
     33 function slugId(name) {
     34   const raw = String(name || "")
     35     .trim()
     36     .toLowerCase()
     37     .replace(/[^a-z0-9]+/g, "-")
     38     .replace(/^-+|-+$/g, "")
     39     .slice(0, 32);
     40   if (!raw) return "";
     41   return /^[a-z0-9][a-z0-9_.-]{0,31}$/.test(raw) ? raw : raw.replace(/[^a-z0-9_.-]/g, "").slice(0, 32);
     42 }
     43 
     44 function postJson(targetUrl, payload) {
     45   return new Promise((resolve) => {
     46     let u;
     47     try {
     48       u = new URL(targetUrl);
     49     } catch {
     50       resolve({ ok: false, status: 0, error: "Invalid URL" });
     51       return;
     52     }
     53 
     54     const transport = u.protocol === "http:" ? http : https;
     55     const port = u.port ? Number(u.port) : u.protocol === "http:" ? 80 : 443;
     56     const body = Buffer.from(JSON.stringify(payload), "utf8");
     57     const req = transport.request(
     58       {
     59         method: "POST",
     60         hostname: u.hostname,
     61         port,
     62         path: u.pathname + u.search,
     63         headers: {
     64           "Content-Type": "application/json",
     65           "Content-Length": body.length,
     66         }
     67       },
     68       (res) => {
     69         let out = "";
     70         res.on("data", (d) => (out += String(d || "")));
     71         res.on("end", () => resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode || 0, body: out }));
     72       }
     73     );
     74     req.on("error", (e) => resolve({ ok: false, status: 0, error: e?.message || String(e) }));
     75     req.end(body);
     76   });
     77 }
     78 
     79 module.exports = function init(api) {
     80   const config = readJson(CONFIG_PATH, {
     81     directoryUrl: "",
     82     instance: { id: "", url: "", name: "", description: "", bzlVersion: "", requiresRegistrationCode: false },
     83     publicHives: []
     84   });
     85 
     86   api.registerWs("getConfig", (ws) => {
     87     if (ws?.user?.role !== "owner") return;
     88     api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:config", config });
     89   });
     90 
     91   api.registerWs("setConfig", (ws, msg) => {
     92     if (ws?.user?.role !== "owner") return;
     93     const next = msg?.config || {};
     94     config.directoryUrl = normalizeUrl(next.directoryUrl || config.directoryUrl);
     95     config.instance = { ...(config.instance || {}), ...(next.instance || {}) };
     96     config.instance.url = normalizeUrl(config.instance.url);
     97     config.instance.id = String(config.instance.id || "").trim();
     98     config.instance.name = String(config.instance.name || "").trim();
     99     config.instance.description = String(config.instance.description || "").trim();
    100     config.instance.bzlVersion = String(config.instance.bzlVersion || "").trim();
    101     config.instance.requiresRegistrationCode = Boolean(config.instance.requiresRegistrationCode);
    102     config.publicHives = Array.isArray(next.publicHives) ? next.publicHives : config.publicHives;
    103     writeJson(CONFIG_PATH, config);
    104     api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:configSaved" });
    105   });
    106 
    107   api.registerWs("publishNow", async (ws) => {
    108     if (ws?.user?.role !== "owner") return;
    109     const urlBase = normalizeUrl(config.directoryUrl);
    110     if (!urlBase) return api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:result", ok: false, error: "Missing directory URL." });
    111     const name = String(config.instance?.name || "").trim();
    112     if (!name) return api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:result", ok: false, error: "Missing instance name." });
    113     const instanceUrl = normalizeUrl(config.instance?.url || "");
    114     if (!instanceUrl) {
    115       return api.sendToUsers([ws.user.username], {
    116         type: "plugin:directory-publisher:result",
    117         ok: false,
    118         error: "Missing instance URL (open publisher tab from the instance and save once)."
    119       });
    120     }
    121     const id = String(config.instance?.id || "").trim() || slugId(name) || "instance";
    122     const instancePayload = {
    123       ...config.instance,
    124       id,
    125       name,
    126       url: instanceUrl,
    127       description: String(config.instance?.description || "").trim(),
    128       bzlVersion: String(config.instance?.bzlVersion || "").trim(),
    129       requiresRegistrationCode: Boolean(config.instance?.requiresRegistrationCode)
    130     };
    131     const endpoint = `${urlBase}/api/plugins/directory-server/announce`;
    132     const payload = { instance: instancePayload, publicHives: config.publicHives };
    133     const r = await postJson(endpoint, payload);
    134     api.sendToUsers([ws.user.username], { type: "plugin:directory-publisher:result", ...r });
    135   });
    136 
    137   api.log("info", "directory-publisher loaded");
    138 };