Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83727957ab | |||
| a8ddae4b1e | |||
| 77134b6bab |
+5
-3
@@ -3,7 +3,7 @@ const { client_logger } = require('../utils/analyticsLogger');
|
|||||||
const bcrypt = require('bcrypt');
|
const bcrypt = require('bcrypt');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils.js');
|
const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils.js');
|
||||||
const { cookiesOptions } = require('../config/cookiesOptions');
|
const { getCookiesOptions } = require('../config/cookiesOptions');
|
||||||
const Notifications = require("../notifications");
|
const Notifications = require("../notifications");
|
||||||
|
|
||||||
// Object Definitions
|
// Object Definitions
|
||||||
@@ -19,6 +19,7 @@ const createPasswordTokenHash = (rawToken) =>
|
|||||||
|
|
||||||
const createSessionFromUser = async ({ DB, user, req, res }) => {
|
const createSessionFromUser = async ({ DB, user, req, res }) => {
|
||||||
const sessionObj = await DB.newSession(user._id);
|
const sessionObj = await DB.newSession(user._id);
|
||||||
|
const cookiesOptions = getCookiesOptions(req);
|
||||||
res.cookie('user_sid', user._id, cookiesOptions);
|
res.cookie('user_sid', user._id, cookiesOptions);
|
||||||
res.cookie('session_id', sessionObj.insertedId, cookiesOptions);
|
res.cookie('session_id', sessionObj.insertedId, cookiesOptions);
|
||||||
const latestUpdatedProfile = await DB.latestProfile(user._id);
|
const latestUpdatedProfile = await DB.latestProfile(user._id);
|
||||||
@@ -143,8 +144,9 @@ const logout = async function (req, res) {
|
|||||||
const session_id = getSessionId(req);
|
const session_id = getSessionId(req);
|
||||||
const user_sid = getUserId(req);
|
const user_sid = getUserId(req);
|
||||||
if (session_id && user_sid) {
|
if (session_id && user_sid) {
|
||||||
res.clearCookie('session_id');
|
const cookiesOptions = getCookiesOptions(req);
|
||||||
res.clearCookie('user_sid');
|
res.clearCookie('session_id', cookiesOptions);
|
||||||
|
res.clearCookie('user_sid', cookiesOptions);
|
||||||
//remove from DB
|
//remove from DB
|
||||||
const DB = await MongoDB.getDB;
|
const DB = await MongoDB.getDB;
|
||||||
DB.removeSession(session_id);
|
DB.removeSession(session_id);
|
||||||
|
|||||||
@@ -1,12 +1,49 @@
|
|||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
const forceSecureCookie = process.env.COOKIE_SECURE === "true";
|
const forceSecureCookie = process.env.COOKIE_SECURE === "true";
|
||||||
const secure = forceSecureCookie || isProduction;
|
|
||||||
|
|
||||||
const cookiesOptions = {
|
const COOKIE_MAX_AGE_MS = 1000 * 60 * 60 * 24 * 90; // 90 days
|
||||||
maxAge: 1000 * 60 * 60 * 24 * 90, // would expire after 90 days
|
const LOCAL_ORIGIN_REGEX = /^http:\/\/(localhost|127\.0\.0\.1|aeropi\.local)(:\d+)?$/i;
|
||||||
httpOnly: true, // The cookie only accessible by the web server
|
const LOCAL_HOST_REGEX = /^(localhost|127\.0\.0\.1|aeropi\.local)(:\d+)?$/i;
|
||||||
sameSite: secure ? 'none' : 'lax',
|
|
||||||
secure,
|
const getHeaderValue = (req, key) => {
|
||||||
|
if (!req || !req.headers) return "";
|
||||||
|
const raw = req.headers[key];
|
||||||
|
if (Array.isArray(raw)) return raw[0] || "";
|
||||||
|
return raw || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { cookiesOptions };
|
const isLocalRequest = (req) => {
|
||||||
|
const origin = getHeaderValue(req, "origin");
|
||||||
|
const host = getHeaderValue(req, "host");
|
||||||
|
return LOCAL_ORIGIN_REGEX.test(origin) || LOCAL_HOST_REGEX.test(host);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isHttpsRequest = (req) => {
|
||||||
|
if (!req) return false;
|
||||||
|
const forwardedProto = String(getHeaderValue(req, "x-forwarded-proto")).split(",")[0].trim().toLowerCase();
|
||||||
|
const reqProtocol = String(req.protocol || "").toLowerCase();
|
||||||
|
const origin = String(getHeaderValue(req, "origin") || "").toLowerCase();
|
||||||
|
if (forwardedProto === "https" || reqProtocol === "https") return true;
|
||||||
|
return origin.startsWith("https://");
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldUseSecureCookie = (req) => {
|
||||||
|
if (forceSecureCookie) return true;
|
||||||
|
if (isLocalRequest(req)) return false;
|
||||||
|
if (isHttpsRequest(req)) return true;
|
||||||
|
return isProduction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCookiesOptions = (req) => {
|
||||||
|
const secure = shouldUseSecureCookie(req);
|
||||||
|
return {
|
||||||
|
maxAge: COOKIE_MAX_AGE_MS,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: secure ? "none" : "lax",
|
||||||
|
secure,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const cookiesOptions = getCookiesOptions();
|
||||||
|
|
||||||
|
module.exports = { cookiesOptions, getCookiesOptions };
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ var corsOptions = {
|
|||||||
'http://127.0.0.1:8081',
|
'http://127.0.0.1:8081',
|
||||||
'http://localhost:3000',
|
'http://localhost:3000',
|
||||||
"https://social.emmint.com",
|
"https://social.emmint.com",
|
||||||
|
"https://www.social.emmint.com",
|
||||||
"https://fellowship.emmint.com",
|
"https://fellowship.emmint.com",
|
||||||
"https://aeropi.local",
|
"https://aeropi.local",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
const DBName = "EMI_SOCIAL";
|
||||||
|
|
||||||
|
const chatDB = (DB) => {
|
||||||
|
DB.chatMessagesCol = DB.db.db(DBName).collection("chat_messages");
|
||||||
|
DB.chatMessagesCol.createIndex({ createdAt: -1 }).catch(console.error);
|
||||||
|
|
||||||
|
DB.addChatMessage = async ({ senderId, senderProfileId, senderName, text, sourceLang }) => {
|
||||||
|
const safeText = (text || "").trim();
|
||||||
|
if (!safeText) return false;
|
||||||
|
const message = {
|
||||||
|
senderId: senderId ? senderId + "" : "",
|
||||||
|
senderProfileId: senderProfileId ? senderProfileId + "" : "",
|
||||||
|
senderName: senderName || "Anonymous",
|
||||||
|
text: safeText,
|
||||||
|
sourceLang: sourceLang || "en",
|
||||||
|
translations: {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
const result = await DB.chatMessagesCol.insertOne(message).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (!result || !result.insertedId) return false;
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
_id: result.insertedId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
DB.getRecentChatMessages = async (limit = 100) => {
|
||||||
|
const safeLimit = Math.min(Math.max(parseInt(limit, 10) || 100, 1), 200);
|
||||||
|
const messages = await DB.chatMessagesCol.find({})
|
||||||
|
.sort({ createdAt: -1 })
|
||||||
|
.limit(safeLimit)
|
||||||
|
.toArray()
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
return messages.reverse();
|
||||||
|
};
|
||||||
|
|
||||||
|
DB.setChatMessageTranslation = async ({ messageId, targetLang, text, provider, model }) => {
|
||||||
|
if (!messageId || !targetLang || !text) return false;
|
||||||
|
const _id = typeof messageId === "string" ? DB.ObjectID(messageId) : messageId;
|
||||||
|
const fieldBase = `translations.${targetLang}`;
|
||||||
|
const update = {
|
||||||
|
$set: {
|
||||||
|
[`${fieldBase}.text`]: text,
|
||||||
|
[`${fieldBase}.provider`]: provider || "openai",
|
||||||
|
[`${fieldBase}.model`]: model || "",
|
||||||
|
[`${fieldBase}.updatedAt`]: new Date(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return DB.chatMessagesCol.updateOne({ _id }, update).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = chatDB;
|
||||||
@@ -183,6 +183,7 @@ const postRoute = require('./routes/post.js');
|
|||||||
const songsRoute = require('./routes/songs.js');
|
const songsRoute = require('./routes/songs.js');
|
||||||
const paymentsRoute = require('./routes/payments.js');
|
const paymentsRoute = require('./routes/payments.js');
|
||||||
const bibleRoute = require('./routes/bible.js');
|
const bibleRoute = require('./routes/bible.js');
|
||||||
|
const chatRoute = require('./routes/chat.js');
|
||||||
const sessionChecker = require('./middleware/sessionChecker');
|
const sessionChecker = require('./middleware/sessionChecker');
|
||||||
// -- Private Routes
|
// -- Private Routes
|
||||||
app.use('/user', sessionChecker, profileRoute);
|
app.use('/user', sessionChecker, profileRoute);
|
||||||
@@ -190,6 +191,7 @@ app.use('/post', sessionChecker, postRoute);
|
|||||||
app.use('/payments', paymentsRoute);
|
app.use('/payments', paymentsRoute);
|
||||||
app.use('/bible', sessionChecker, bibleRoute);
|
app.use('/bible', sessionChecker, bibleRoute);
|
||||||
app.use('/songs', sessionChecker, songsRoute);
|
app.use('/songs', sessionChecker, songsRoute);
|
||||||
|
app.use('/chat', sessionChecker, chatRoute);
|
||||||
// -- Public Routes
|
// -- Public Routes
|
||||||
const subsplashRoute = require('./routes/subsplash.js');
|
const subsplashRoute = require('./routes/subsplash.js');
|
||||||
app.use('/subsplash', subsplashRoute);
|
app.use('/subsplash', subsplashRoute);
|
||||||
@@ -238,7 +240,7 @@ const webPushEmail = process.env.WEB_PUSH_EMAIL;
|
|||||||
webPush.setVapidDetails('mailto:' + webPushEmail, publicVapidKey, privateVapidKey);
|
webPush.setVapidDetails('mailto:' + webPushEmail, publicVapidKey, privateVapidKey);
|
||||||
|
|
||||||
|
|
||||||
const { cookiesOptions } = require('./config/cookiesOptions');
|
const { getCookiesOptions } = require('./config/cookiesOptions');
|
||||||
const { client_logger } = require('./utils/analyticsLogger.js');
|
const { client_logger } = require('./utils/analyticsLogger.js');
|
||||||
const { getSessionId, getUserId, getProfileId } = require('./utils/sessionUtils.js');
|
const { getSessionId, getUserId, getProfileId } = require('./utils/sessionUtils.js');
|
||||||
|
|
||||||
@@ -408,7 +410,7 @@ DB.getDB.then((DB) => {
|
|||||||
return res.status(403).json({ status: "Profile does not belong to the logged-in user" });
|
return res.status(403).json({ status: "Profile does not belong to the logged-in user" });
|
||||||
}
|
}
|
||||||
// Update active profile cookie
|
// Update active profile cookie
|
||||||
res.cookie('profile_id', profile._id, cookiesOptions);
|
res.cookie('profile_id', profile._id, getCookiesOptions(req));
|
||||||
return res.json({ status: "ok", profile });
|
return res.json({ status: "ok", profile });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error changing profile:", error);
|
console.error("Error changing profile:", error);
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils');
|
const { getSessionId, getUserId, getProfileId } = require('../utils/sessionUtils');
|
||||||
const { client_logger } = require('../utils/analyticsLogger');
|
const { client_logger } = require('../utils/analyticsLogger');
|
||||||
const { cookiesOptions } = require('../config/cookiesOptions');
|
const { getCookiesOptions } = require('../config/cookiesOptions');
|
||||||
const MongoDB = require("../mongoDB.js");
|
const MongoDB = require("../mongoDB.js");
|
||||||
const { ObjectId } = require("mongodb");
|
const { ObjectId } = require("mongodb");
|
||||||
|
|
||||||
|
const shouldReturnJson = (req) => {
|
||||||
|
const accept = String(req?.headers?.accept || "").toLowerCase();
|
||||||
|
const contentType = String(req?.headers?.["content-type"] || "").toLowerCase();
|
||||||
|
return !!req?.headers?.origin || accept.includes("application/json") || contentType.includes("application/json");
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectUnauthorized = (req, res) => {
|
||||||
|
if (shouldReturnJson(req)) {
|
||||||
|
return res.status(401).json({ status: "Unauthorized" });
|
||||||
|
}
|
||||||
|
return res.redirect('/login');
|
||||||
|
};
|
||||||
|
|
||||||
const sessionChecker = async (req, res, next) => {
|
const sessionChecker = async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const session_id = getSessionId(req);
|
const session_id = getSessionId(req);
|
||||||
@@ -11,10 +24,10 @@ const sessionChecker = async (req, res, next) => {
|
|||||||
let profile_id = getProfileId(req);
|
let profile_id = getProfileId(req);
|
||||||
|
|
||||||
if (!session_id || !user_sid) {
|
if (!session_id || !user_sid) {
|
||||||
return res.redirect('/login');
|
return rejectUnauthorized(req, res);
|
||||||
}
|
}
|
||||||
if (!ObjectId.isValid(session_id) || !ObjectId.isValid(user_sid)) {
|
if (!ObjectId.isValid(session_id) || !ObjectId.isValid(user_sid)) {
|
||||||
return res.redirect('/login');
|
return rejectUnauthorized(req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DB = await MongoDB.getDB;
|
const DB = await MongoDB.getDB;
|
||||||
@@ -24,15 +37,15 @@ const sessionChecker = async (req, res, next) => {
|
|||||||
if (!await DB.getProfileCache(profile_id)) {
|
if (!await DB.getProfileCache(profile_id)) {
|
||||||
const latestProfile = await DB.latestProfile(user_sid);
|
const latestProfile = await DB.latestProfile(user_sid);
|
||||||
if (!latestProfile || !latestProfile._id) {
|
if (!latestProfile || !latestProfile._id) {
|
||||||
return res.redirect('/login');
|
return rejectUnauthorized(req, res);
|
||||||
}
|
}
|
||||||
res.cookie('profile_id', latestProfile._id, cookiesOptions);
|
res.cookie('profile_id', latestProfile._id, getCookiesOptions(req));
|
||||||
profile_id = latestProfile._id;
|
profile_id = latestProfile._id;
|
||||||
}
|
}
|
||||||
|
|
||||||
req.profileInfo = { _id: profile_id };
|
req.profileInfo = { _id: profile_id };
|
||||||
|
|
||||||
if (!userInfo) return res.redirect('/login');
|
if (!userInfo) return rejectUnauthorized(req, res);
|
||||||
|
|
||||||
client_logger.capture({
|
client_logger.capture({
|
||||||
distinctId: user_sid,
|
distinctId: user_sid,
|
||||||
@@ -42,7 +55,7 @@ const sessionChecker = async (req, res, next) => {
|
|||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Session checker error", error);
|
console.error("Session checker error", error);
|
||||||
return res.redirect('/login');
|
return rejectUnauthorized(req, res);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const postDB = require("./dbTools/post.js");
|
|||||||
const profileDB = require("./dbTools/profile.js");
|
const profileDB = require("./dbTools/profile.js");
|
||||||
const paymentDB = require("./dbTools/payments.js");
|
const paymentDB = require("./dbTools/payments.js");
|
||||||
const songsDB = require("./dbTools/songs.js");
|
const songsDB = require("./dbTools/songs.js");
|
||||||
|
const chatDB = require("./dbTools/chat.js");
|
||||||
|
|
||||||
console.log("Connecting to MongoDB...");
|
console.log("Connecting to MongoDB...");
|
||||||
const nodeMajorVersion = parseInt((process.versions.node || "0").split(".")[0], 10);
|
const nodeMajorVersion = parseInt((process.versions.node || "0").split(".")[0], 10);
|
||||||
@@ -177,6 +178,7 @@ const getDB = new Promise((resolve, reject) => {
|
|||||||
profileDB(DB);
|
profileDB(DB);
|
||||||
paymentDB(DB);
|
paymentDB(DB);
|
||||||
songsDB(DB);
|
songsDB(DB);
|
||||||
|
chatDB(DB);
|
||||||
|
|
||||||
resolve(DB);
|
resolve(DB);
|
||||||
});
|
});
|
||||||
|
|||||||
+238
@@ -0,0 +1,238 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
const DEFAULT_MODEL = process.env.OPENAI_TRANSLATION_MODEL || process.env.OPENAI_MODEL || "gpt-4o-mini";
|
||||||
|
|
||||||
|
const normalizeLanguageCode = (rawLanguage) => {
|
||||||
|
if (!rawLanguage || typeof rawLanguage !== "string") return "en";
|
||||||
|
const firstValue = rawLanguage.split(",")[0].trim().toLowerCase();
|
||||||
|
if (!firstValue) return "en";
|
||||||
|
const noQuality = firstValue.split(";")[0].trim();
|
||||||
|
const shortCode = noQuality.split("-")[0].trim();
|
||||||
|
return shortCode || "en";
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractOutputText = (data) => {
|
||||||
|
if (!data) return "";
|
||||||
|
if (typeof data.output_text === "string" && data.output_text.trim()) {
|
||||||
|
return data.output_text.trim();
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data.output)) return "";
|
||||||
|
const chunks = [];
|
||||||
|
data.output.forEach((item) => {
|
||||||
|
if (!Array.isArray(item?.content)) return;
|
||||||
|
item.content.forEach((entry) => {
|
||||||
|
if (entry?.type === "output_text" && typeof entry?.text === "string") {
|
||||||
|
chunks.push(entry.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return chunks.join("\n").trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const translateText = async ({ text, sourceLang, targetLang }) => {
|
||||||
|
const normalizedSource = normalizeLanguageCode(sourceLang);
|
||||||
|
const normalizedTarget = normalizeLanguageCode(targetLang);
|
||||||
|
if (!text || !normalizedTarget || normalizedSource === normalizedTarget) {
|
||||||
|
return {
|
||||||
|
translatedText: text,
|
||||||
|
provider: "none",
|
||||||
|
model: "none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = process.env.OPENAI_API_KEY;
|
||||||
|
if (!apiKey) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
"https://api.openai.com/v1/responses",
|
||||||
|
{
|
||||||
|
model: DEFAULT_MODEL,
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "input_text",
|
||||||
|
text: "You translate chat messages. Keep meaning, tone, emojis, names, and references. Return only the translated text.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "input_text",
|
||||||
|
text: `Translate this message from ${normalizedSource} to ${normalizedTarget}:\n\n${text}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 15000,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const translatedText = extractOutputText(response?.data);
|
||||||
|
if (!translatedText) return null;
|
||||||
|
return {
|
||||||
|
translatedText,
|
||||||
|
provider: "openai",
|
||||||
|
model: DEFAULT_MODEL,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error translating chat message", error?.response?.data || error?.message || error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
normalizeLanguageCode,
|
||||||
|
translateText,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user