var express = require('express'); var router = express.Router(); const DB = require("../mongoDB.js"); const { getUserId, getProfileId } = require("../utils/sessionUtils.js"); const { normalizeLanguageCode, translateText } = require("../utils/chatTranslation.js"); const ACTIVE_WINDOW_MS = 120000; const MESSAGE_MAX_LENGTH = 500; const activeUsers = new Map(); const translationInflight = new Map(); const toDisplayName = (profile, fallbackName) => { const firstName = profile?.profile?.firstName || ""; const lastName = profile?.profile?.lastName || ""; const displayName = (firstName + " " + lastName).trim(); return displayName || fallbackName || "Anonymous"; }; const pruneActiveUsers = () => { const now = Date.now(); for (const [profileId, entry] of activeUsers.entries()) { if (now - entry.lastSeen > ACTIVE_WINDOW_MS) { activeUsers.delete(profileId); } } }; const getActiveUsersList = () => { pruneActiveUsers(); return Array.from(activeUsers.values()) .sort((a, b) => b.lastSeen - a.lastSeen) .map((entry) => ({ profileId: entry.profileId, userId: entry.userId, displayName: entry.displayName, lastSeen: entry.lastSeen, })); }; DB.getDB.then((DB) => { const resolveTargetLanguage = (req) => { const requested = req.query?.lang || req.headers["x-app-language"] || req.headers["accept-language"] || "en"; return normalizeLanguageCode(requested); }; const mapChatMessageForLanguage = async (message, targetLang) => { const normalizedTarget = normalizeLanguageCode(targetLang); const sourceLang = normalizeLanguageCode(message?.sourceLang || "auto"); const originalText = message?.text || ""; if (!originalText) { return { ...message, textOriginal: "", text: "", displayLang: sourceLang, }; } if (sourceLang === normalizedTarget) { return { ...message, textOriginal: originalText, text: originalText, displayLang: sourceLang, }; } const cachedTranslation = message?.translations?.[normalizedTarget]?.text; if (cachedTranslation) { return { ...message, textOriginal: originalText, text: cachedTranslation, displayLang: normalizedTarget, }; } const translationKey = `${message?._id?.toString?.() || ""}:${normalizedTarget}`; if (translationInflight.has(translationKey)) { await translationInflight.get(translationKey); const refreshed = await DB.chatMessagesCol.findOne({ _id: message._id }).catch(() => null); const refreshedCached = refreshed?.translations?.[normalizedTarget]?.text; if (refreshedCached) { return { ...message, translations: refreshed.translations, textOriginal: originalText, text: refreshedCached, displayLang: normalizedTarget, }; } return { ...message, textOriginal: originalText, text: originalText, displayLang: sourceLang, }; } const inFlightTask = (async () => { const translated = await translateText({ text: originalText, sourceLang, targetLang: normalizedTarget, }); if (!translated?.translatedText) return null; await DB.setChatMessageTranslation({ messageId: message._id, targetLang: normalizedTarget, text: translated.translatedText, provider: translated.provider, model: translated.model, }); return translated.translatedText; })(); translationInflight.set(translationKey, inFlightTask); let translatedText = null; try { translatedText = await inFlightTask; } finally { translationInflight.delete(translationKey); } return { ...message, textOriginal: originalText, text: translatedText || originalText, displayLang: translatedText ? normalizedTarget : sourceLang, }; }; const markActiveUser = async (req) => { const userId = getUserId(req); const profileId = req.profileInfo?._id || getProfileId(req); if (!profileId || !userId) return null; const profile = await DB.getProfileCache(profileId); const displayName = toDisplayName(profile, req.userInfo?.username); activeUsers.set(profileId + "", { profileId: profileId + "", userId: userId + "", displayName, lastSeen: Date.now(), }); return activeUsers.get(profileId + ""); }; router.get("/messages", async (req, res) => { try { await markActiveUser(req); const targetLang = resolveTargetLanguage(req); const messages = await DB.getRecentChatMessages(req.query.limit || 100); const translatedMessages = await Promise.all(messages.map((message) => mapChatMessageForLanguage(message, targetLang))); return res.json({ status: "ok", requestedLang: targetLang, messages: translatedMessages, }); } catch (error) { console.error("Error getting chat messages", error); return res.status(500).json({ status: "Internal server error", messages: [] }); } }); router.post("/messages", async (req, res) => { try { const userId = getUserId(req); const profileId = req.profileInfo?._id || getProfileId(req); const text = typeof req.body?.text === "string" ? req.body.text.trim() : ""; const sourceLang = normalizeLanguageCode(req.body?.sourceLang || req.headers["x-app-language"] || "en"); if (!text) { return res.status(400).json({ status: "Message text is required" }); } if (text.length > MESSAGE_MAX_LENGTH) { return res.status(400).json({ status: `Message too long (${MESSAGE_MAX_LENGTH} max chars)` }); } const profile = await DB.getProfileCache(profileId); const senderName = toDisplayName(profile, req.userInfo?.username); const message = await DB.addChatMessage({ senderId: userId, senderProfileId: profileId, senderName, text, sourceLang, }); if (!message) { return res.status(500).json({ status: "Could not save message" }); } activeUsers.set(profileId + "", { profileId: profileId + "", userId: userId + "", displayName: senderName, lastSeen: Date.now(), }); return res.json({ status: "ok", message, activeUsers: getActiveUsersList(), }); } catch (error) { console.error("Error posting chat message", error); return res.status(500).json({ status: "Internal server error" }); } }); router.get("/active", async (req, res) => { try { await markActiveUser(req); return res.json({ status: "ok", activeUsers: getActiveUsersList(), }); } catch (error) { console.error("Error getting active chat users", error); return res.status(500).json({ status: "Internal server error", activeUsers: [] }); } }); router.post("/ping", async (req, res) => { try { await markActiveUser(req); return res.json({ status: "ok", activeUsers: getActiveUsersList(), }); } catch (error) { console.error("Error updating chat presence", error); return res.status(500).json({ status: "Internal server error", activeUsers: [] }); } }); }); module.exports = router;