add chat system

This commit is contained in:
KingKDot
2026-04-14 21:56:18 -05:00
parent 6f14bf35e2
commit eb6ec86773
20 changed files with 1211 additions and 5 deletions
+68 -2
View File
@@ -4,7 +4,7 @@ import { logger } from "./logger";
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs/promises";
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getBuildByTag, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, clearNotificationScreenshots, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles } from "./db";
import { upsertClientRow, setOnlineState, listClients, markAllClientsOffline, getBuild, getBuildByTag, getAllBuilds, deleteExpiredBuilds, deleteBuild, getNotificationScreenshot, clearNotificationScreenshots, deleteClientRow, getClientIp, banIp, isIpBanned, clientExists, deleteExpiredSharedFiles, getChatHistory, insertChatMessage, getOnlineClientCountForUser, deleteExpiredChatMessages } from "./db";
import { handleFrame, handleHello, handlePing, handlePong } from "./wsHandlers";
import { getMessageByteLength, getMaxPayloadLimit, isAllowedClientMessageType } from "./wsValidation";
import { ClientInfo, ClientRole } from "./types";
@@ -12,13 +12,14 @@ import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "./auth";
import { loadConfig, getConfig } from "./config";
import { flushAuditLogsSync } from "./auditLog";
import { getUserById, getUsersForNotificationDelivery, getUsersForNotificationDeliveryByClient, canUserAccessClient, setUserClientAccessRule, setUserClientAccessScope, getUserClientAccessScope } from "./users";
import { getUserById, getUsersForNotificationDelivery, getUsersForNotificationDeliveryByClient, canUserAccessClient, setUserClientAccessRule, setUserClientAccessScope, getUserClientAccessScope, hasPermission } from "./users";
import { requireAuth, requirePermission } from "./rbac";
import { metrics } from "./metrics";
import { ensureDataDir } from "./paths";
import { handleAuthRoutes } from "./server/routes/auth-routes";
import { handleAutoScriptsRoutes } from "./server/routes/auto-scripts-routes";
import { handleEnrollmentRoutes, setPostApproveHook } from "./server/routes/enrollment-routes";
import { handleChatRoutes } from "./server/routes/chat-routes";
import { handleBuildRoutes } from "./server/routes/build-routes";
import { handleSolRoutes } from "./server/routes/sol-routes";
import { handleAssetsRoutes } from "./server/routes/assets-routes";
@@ -507,6 +508,54 @@ async function startServer() {
});
},
handleNotificationViewerOpen: notificationPluginHandlers.handleNotificationViewerOpen,
handleChatViewerOpen: (ws: import("bun").ServerWebSocket<SocketData>) => {
const id = crypto.randomUUID();
ws.data.sessionId = id;
const userId = ws.data.userId;
const userRole = ws.data.userRole || "viewer";
const user = userId ? getUserById(userId) : null;
const username = user?.username || "Unknown";
sessionManager.addChatSession({
id,
viewer: ws,
createdAt: Date.now(),
userId: userId || 0,
username,
userRole,
});
const retDays = getConfig().chat?.retentionDays ?? 30;
const retMs = retDays > 0 ? retDays * 24 * 60 * 60 * 1000 : undefined;
const history = getChatHistory(undefined, 50, retMs);
const enrichedHistory = history.map((m) => ({
...m,
onlineClients: m.userId ? getOnlineClientCountForUser(m.userId) : 0,
}));
const canWrite = userId ? hasPermission(userRole as any, "chat:write", userId) : false;
ws.send(JSON.stringify({ type: "chat_ready", history: enrichedHistory, canWrite, userId: userId || 0 }));
},
handleChatViewerMessage: (ws: import("bun").ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) => {
try {
const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw as ArrayBuffer);
const parsed = JSON.parse(text);
if (parsed.type !== "chat_send" || typeof parsed.message !== "string") return;
const userId = ws.data.userId;
const userRole = ws.data.userRole || "viewer";
if (!userId || !hasPermission(userRole as any, "chat:write", userId)) {
ws.send(JSON.stringify({ type: "chat_error", error: "Permission denied" }));
return;
}
const msg = parsed.message.trim();
if (!msg || msg.length > 2000) {
ws.send(JSON.stringify({ type: "chat_error", error: msg ? "Message too long (max 2000 chars)" : "Message is empty" }));
return;
}
const user = getUserById(userId);
const username = user?.username || "Unknown";
const record = insertChatMessage(userId, username, userRole, msg);
const onlineClients = getOnlineClientCountForUser(userId);
sessionManager.broadcastChatMessage(JSON.stringify({ type: "chat_message", ...record, onlineClients }));
} catch {}
},
handleConsoleViewerMessage,
handleRemoteDesktopViewerMessage,
handleWebcamViewerMessage,
@@ -586,6 +635,7 @@ async function startServer() {
handleAutoScriptsRoutes,
handleAutoDeployRoutes,
handleEnrollmentRoutes,
handleChatRoutes,
handleSolRoutes,
handleUsersRoutes,
handleBuildRoutes,
@@ -625,6 +675,22 @@ async function startServer() {
}
if (expiredPaths.length) logger.info(`[db] Cleaned up ${expiredPaths.length} expired shared files`);
const chatRetentionDays = getConfig().chat?.retentionDays ?? 30;
if (chatRetentionDays > 0) {
const retentionMs = chatRetentionDays * 24 * 60 * 60 * 1000;
const purged = deleteExpiredChatMessages(retentionMs);
if (purged > 0) logger.info(`[db] Cleaned up ${purged} expired chat messages (retention: ${chatRetentionDays}d)`);
}
setInterval(() => {
const days = getConfig().chat?.retentionDays ?? 30;
if (days > 0) {
const ms = days * 24 * 60 * 60 * 1000;
const count = deleteExpiredChatMessages(ms);
if (count > 0) logger.info(`[chat] Purged ${count} expired messages (retention: ${days}d)`);
}
}, 60 * 60 * 1000); // every hour
startMaintenanceLoops({
getClients: clientManager.getAllClients,
setOnlineState,