DELETE /rigshare/api/chat (admin) empties chat.jsonl, tells every client to clear its history, then posts a notice saying who cleared it. Admin tab gets a Chat section with a confirmed Clear chat history button.
234 lines
7.6 KiB
JavaScript
234 lines
7.6 KiB
JavaScript
// Connection, identity and HTTP helpers shared by every RigShare module.
|
|
|
|
const LS = {
|
|
token: "rigshare.token",
|
|
guestId: "rigshare.guestId",
|
|
guestName: "rigshare.guestName",
|
|
};
|
|
|
|
function storageGet(key) {
|
|
try { return localStorage.getItem(key); } catch { return null; }
|
|
}
|
|
|
|
function storageSet(key, value) {
|
|
try {
|
|
if (value == null) localStorage.removeItem(key);
|
|
else localStorage.setItem(key, value);
|
|
} catch { /* private mode */ }
|
|
}
|
|
|
|
function randomId() {
|
|
return crypto.randomUUID?.() ?? Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
}
|
|
|
|
export class RigClient extends EventTarget {
|
|
constructor() {
|
|
super();
|
|
this.ws = null;
|
|
this.status = "offline"; // offline | connecting | online | denied
|
|
this.self = null;
|
|
this.users = [];
|
|
this.chat = []; // loaded chat messages, oldest first
|
|
this.chatMore = false; // older messages exist on the server
|
|
this.rooms = [];
|
|
this.stats = null;
|
|
this.server = {};
|
|
this.retry = 0;
|
|
this.token = storageGet(LS.token);
|
|
this.guestId = storageGet(LS.guestId) || randomId();
|
|
storageSet(LS.guestId, this.guestId);
|
|
this.guestName = storageGet(LS.guestName) || "";
|
|
}
|
|
|
|
get perms() {
|
|
return this.self?.perms ?? { edit: false, queue: false, admin: false };
|
|
}
|
|
|
|
get online() {
|
|
return this.status === "online";
|
|
}
|
|
|
|
emit(type, detail) {
|
|
this.dispatchEvent(new CustomEvent(type, { detail }));
|
|
}
|
|
|
|
on(type, fn) {
|
|
this.addEventListener(type, (e) => fn(e.detail));
|
|
}
|
|
|
|
// ----- websocket ------------------------------------------------------
|
|
|
|
connect() {
|
|
clearTimeout(this.reconnectTimer);
|
|
const base = location.pathname.replace(/[^/]*$/, "");
|
|
const url = `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${base}rigshare/ws`;
|
|
this.setStatus("connecting");
|
|
const ws = new WebSocket(url);
|
|
this.ws = ws;
|
|
ws.onopen = () => {
|
|
this.retry = 0;
|
|
this.hello();
|
|
};
|
|
ws.onmessage = (e) => {
|
|
let msg;
|
|
try { msg = JSON.parse(e.data); } catch { return; }
|
|
this.handle(msg);
|
|
};
|
|
ws.onclose = () => {
|
|
if (this.ws !== ws) return;
|
|
if (this.status !== "denied") this.setStatus("offline");
|
|
const delay = Math.min(15000, 1000 * 2 ** this.retry++);
|
|
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
};
|
|
}
|
|
|
|
hello() {
|
|
this.send({ type: "hello", token: this.token, guest_id: this.guestId, name: this.guestName });
|
|
}
|
|
|
|
send(msg) {
|
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
this.ws.send(JSON.stringify(msg));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
setStatus(status) {
|
|
if (this.status === status) return;
|
|
this.status = status;
|
|
this.emit("status", status);
|
|
}
|
|
|
|
handle(msg) {
|
|
switch (msg.type) {
|
|
case "welcome":
|
|
this.self = msg.self;
|
|
this.users = msg.users;
|
|
this.chat = msg.chat || [];
|
|
this.chatMore = !!msg.chat_more;
|
|
this.rooms = msg.rooms || [];
|
|
this.stats = msg.stats;
|
|
this.server = msg.server || {};
|
|
this.setStatus("online");
|
|
this.emit("self", this.self);
|
|
this.emit("presence", this.users);
|
|
this.emit("chat-history", this.chat);
|
|
this.emit("welcome", msg);
|
|
this.emit("server", this.server);
|
|
break;
|
|
case "denied":
|
|
this.self = null;
|
|
this.denyReason = msg.reason;
|
|
this.setStatus("denied");
|
|
this.emit("self", null);
|
|
if (msg.login) this.gotoLogin();
|
|
break;
|
|
case "self":
|
|
this.self = msg.self;
|
|
this.emit("self", this.self);
|
|
break;
|
|
case "logged_out":
|
|
this.setToken(null);
|
|
this.gotoLogin();
|
|
this.emit("toast", { severity: "warn", summary: "RigShare", detail: "Your session ended. You are now a guest." });
|
|
break;
|
|
case "presence":
|
|
this.users = msg.users;
|
|
this.rooms = msg.rooms || this.rooms;
|
|
this.emit("presence", this.users);
|
|
break;
|
|
case "server":
|
|
this.server = msg.server || {};
|
|
this.emit("server", this.server);
|
|
break;
|
|
case "stats":
|
|
this.stats = msg.stats;
|
|
this.emit("stats", msg.stats);
|
|
break;
|
|
case "chat_cleared":
|
|
this.chat = [];
|
|
this.chatMore = false;
|
|
this.emit("chat-history", this.chat);
|
|
break;
|
|
case "chat":
|
|
this.chat.push(msg.message);
|
|
this.emit("chat", msg.message);
|
|
break;
|
|
default:
|
|
this.emit(msg.type, msg);
|
|
}
|
|
}
|
|
|
|
// ----- identity -------------------------------------------------------
|
|
|
|
setToken(token) {
|
|
this.token = token;
|
|
storageSet(LS.token, token);
|
|
}
|
|
|
|
async login(username, password) {
|
|
const res = await this.request("POST", "/rigshare/api/login", { username, password });
|
|
this.setToken(res.token);
|
|
this.reconnect();
|
|
return res.user;
|
|
}
|
|
|
|
async logout() {
|
|
try { await this.request("POST", "/rigshare/api/logout"); } catch { /* ignore */ }
|
|
this.setToken(null);
|
|
this.gotoLogin();
|
|
}
|
|
|
|
gotoLogin() {
|
|
const base = location.pathname.replace(/[^/]*$/, "");
|
|
const next = encodeURIComponent(location.pathname + location.search + location.hash);
|
|
location.replace(`${base}rigshare/login?next=${next}`);
|
|
}
|
|
|
|
rename(name) {
|
|
this.guestName = name;
|
|
storageSet(LS.guestName, name);
|
|
this.send({ type: "rename", name });
|
|
}
|
|
|
|
reconnect() {
|
|
const ws = this.ws;
|
|
this.ws = null;
|
|
this.status = "offline";
|
|
ws?.close();
|
|
this.connect();
|
|
}
|
|
|
|
// Headers that identify this browser to the ComfyUI HTTP API.
|
|
authHeaders() {
|
|
if (this.token) return { "X-RigShare-Token": this.token };
|
|
return { "X-RigShare-Guest": this.guestId };
|
|
}
|
|
|
|
/** Fetch the page of chat before the oldest loaded message; returns them (oldest first). */
|
|
async loadOlderChat(limit = 50) {
|
|
const oldest = this.chat[0]?.seq;
|
|
if (!this.chatMore || oldest === undefined) return [];
|
|
const data = await this.request("GET", `/rigshare/api/chat?before=${oldest}&limit=${limit}`);
|
|
// A reconnect may have replaced the history meanwhile.
|
|
if (this.chat[0]?.seq !== oldest) return [];
|
|
this.chat.unshift(...data.messages);
|
|
this.chatMore = data.more;
|
|
return data.messages;
|
|
}
|
|
|
|
async request(method, path, body) {
|
|
const base = location.pathname.replace(/[^/]*$/, "");
|
|
const res = await fetch(base + path.replace(/^\//, ""), {
|
|
method,
|
|
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
let data = null;
|
|
try { data = await res.json(); } catch { /* empty */ }
|
|
if (!res.ok) throw new Error(data?.error || `${res.status} ${res.statusText}`);
|
|
return data;
|
|
}
|
|
}
|