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 };