Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"predev": "node scripts/free-ports.js 8080",
"dev": "nodemon --watch server --watch .env --ignore logs --ignore uploads server",
"build": "node --check server/index.js && node --check server/server.js && node --check server/bootstrap.js && node --check server/config.js && node --check server/db/sequelize.js && node --check server/db/connect.js && node --check server/db/models/inbox.js && node --check server/db/models/chat.js && node --check server/db/models/chatRoomCounter.js && node --check server/db/models/messageReceipt.js && node --check server/db/models/chatDraft.js && node --check server/db/models/messageRequest.js && node --check server/db/models/chatTopic.js && node --check server/db/models/e2eeDeviceKey.js && node --check server/db/models/resumableUpload.js && node --check server/db/models/chatAiConfig.js && node --check server/db/models/adminSocialAuthConfig.js && node --check server/db/models/profile.js && node --check server/db/models/group.js && node --check server/db/models/channel.js && node --check server/db/models/adminStorageConfig.js && node --check server/db/models/adminCallConfig.js && node --check server/db/models/callHistory.js && node --check server/db/models/profilePhoto.js && node --check server/db/models/nativePushDevice.js && node --check server/helpers/storageConfig.js && node --check server/helpers/callConfig.js && node --check server/helpers/callHistory.js && node --check server/helpers/callState.js && node --check server/helpers/avatarDefaults.js && node --check server/helpers/ensureAvatarDefaults.js && node --check server/helpers/ensureProfile.js && node --check server/helpers/privacy.js && node --check server/helpers/profilePhotos.js && node --check server/helpers/livekit.js && node --check server/helpers/nativePushConfig.js && node --check server/helpers/nativePush.js && node --check server/helpers/storage.js && node --check server/helpers/videoPipeline.js && node --check server/helpers/accountArchive.js && node --check server/helpers/accountExport.js && node --check server/helpers/logger.js && node --check server/helpers/chatAbuse.js && node --check server/helpers/chatMentions.js && node --check server/helpers/messageRequests.js && node --check server/helpers/chatReliability.js && node --check server/helpers/chatAiConfig.js && node --check server/helpers/socialAuthConfig.js && node --check server/helpers/e2eeKeyDirectory.js && node --check server/helpers/chatMaintenance.js && node --check server/middleware/chatSendIdempotency.js && node --check server/controllers/avatar.js && node --check server/controllers/profile.js && node --check server/controllers/chatUpload.js && node --check server/controllers/chatSecurity.js && node --check server/controllers/chatDeletion.js && node --check server/controllers/chatV2.js && node --check server/controllers/chatAiAdmin.js && node --check server/controllers/chatSuggestions.js && node --check server/controllers/chatResumableUpload.js && node --check server/controllers/socialAuth.js && node --check server/controllers/socialAuthAdmin.js && node --check server/controllers/storageAdmin.js && node --check server/controllers/callingAdmin.js && node --check server/controllers/callingPushAdmin.js && node --check server/controllers/callingConfig.js && node --check server/controllers/nativePush.js && node --check server/controllers/accountStorage.js && node --check server/routes/profile.js && node --check server/routes/inbox.js && node --check server/routes/chat.js && node --check server/routes/chatV2.js && node --check server/routes/chatAiAdmin.js && node --check server/routes/socialAuthAdmin.js && node --check server/routes/storageAdmin.js && node --check server/routes/callingAdmin.js && node --check server/routes/callingPushAdmin.js && node --check server/routes/callingConfig.js && node --check server/routes/setting.js && node --check server/helpers/socketAdapter.js && node --check server/socket/auth.js && node --check server/socket/index.js && node --check server/socket/events/chatV2.js && node --check server/socket/events/calling.js && node --check server/socket/events/backgroundCalling.js && node --check server/socket/events/groupModeration.js && node --check server/socket/events/room.js && node --check server/routes/cron.js && node --check api/index.js",
"build": "node --check server/index.js && node --check server/server.js && node --check server/bootstrap.js && node --check server/config.js && node --check server/db/sequelize.js && node --check server/db/connect.js && node --check server/db/models/inbox.js && node --check server/db/models/chat.js && node --check server/db/models/chatRoomCounter.js && node --check server/db/models/messageReceipt.js && node --check server/db/models/chatDraft.js && node --check server/db/models/messageRequest.js && node --check server/db/models/chatTopic.js && node --check server/db/models/e2eeDeviceKey.js && node --check server/db/models/resumableUpload.js && node --check server/db/models/chatAiConfig.js && node --check server/db/models/adminSocialAuthConfig.js && node --check server/db/models/profile.js && node --check server/db/models/group.js && node --check server/db/models/channel.js && node --check server/db/models/adminStorageConfig.js && node --check server/db/models/adminCallConfig.js && node --check server/db/models/callHistory.js && node --check server/db/models/profilePhoto.js && node --check server/db/models/nativePushDevice.js && node --check server/helpers/storageConfig.js && node --check server/helpers/callConfig.js && node --check server/helpers/callHistory.js && node --check server/helpers/callState.js && node --check server/helpers/avatarDefaults.js && node --check server/helpers/ensureAvatarDefaults.js && node --check server/helpers/ensureProfile.js && node --check server/helpers/privacy.js && node --check server/helpers/profilePhotos.js && node --check server/helpers/livekit.js && node --check server/helpers/nativePushConfig.js && node --check server/helpers/nativePush.js && node --check server/helpers/storage.js && node --check server/helpers/videoPipeline.js && node --check server/helpers/accountArchive.js && node --check server/helpers/accountExport.js && node --check server/helpers/logger.js && node --check server/helpers/chatAbuse.js && node --check server/helpers/chatMentions.js && node --check server/helpers/messageRequests.js && node --check server/helpers/chatReliability.js && node --check server/helpers/chatAiConfig.js && node --check server/helpers/socialAuthConfig.js && node --check server/helpers/e2eeKeyDirectory.js && node --check server/helpers/chatMaintenance.js && node --check server/middleware/chatSendIdempotency.js && node --check server/controllers/avatar.js && node --check server/controllers/profile.js && node --check server/controllers/chatUpload.js && node --check server/controllers/chatSecurity.js && node --check server/controllers/chatDeletion.js && node --check server/controllers/chatV2.js && node --check server/controllers/chatAiAdmin.js && node --check server/controllers/chatSuggestions.js && node --check server/controllers/chatResumableUpload.js && node --check server/controllers/socialAuth.js && node --check server/controllers/socialAuthAdmin.js && node --check server/controllers/storageAdmin.js && node --check server/controllers/callingAdmin.js && node --check server/controllers/callingPushAdmin.js && node --check server/controllers/callingConfig.js && node --check server/controllers/nativePush.js && node --check server/controllers/accountStorage.js && node --check server/routes/profile.js && node --check server/routes/inbox.js && node --check server/routes/chat.js && node --check server/routes/chatV2.js && node --check server/routes/chatAiAdmin.js && node --check server/routes/socialAuthAdmin.js && node --check server/routes/storageAdmin.js && node --check server/routes/callingAdmin.js && node --check server/routes/callingPushAdmin.js && node --check server/routes/callingConfig.js && node --check server/routes/setting.js && node --check server/helpers/socketAdapter.js && node --check server/socket/auth.js && node --check server/socket/index.js && node --check server/socket/events/chatV2.js && node --check server/socket/events/calling.js && node --check server/socket/events/callRealtime.js && node --check server/socket/events/backgroundCalling.js && node --check server/socket/events/groupModeration.js && node --check server/socket/events/room.js && node --check server/routes/cron.js && node --check api/index.js",
"start": "node server"
},
"dependencies": {
Expand Down
11 changes: 8 additions & 3 deletions backend/server/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const {
startScheduledMessageWorker,
} = require('./helpers/scheduledMessages');
const { configureSocketAdapter } = require('./helpers/socketAdapter');
const { getCallConfig } = require('./helpers/callConfig');
const ensureAvatarDefaults = require('./helpers/ensureAvatarDefaults');
const ensureChatIndexes = require('./helpers/chatIndexes');
const logger = require('./helpers/logger');
Expand All @@ -14,13 +15,17 @@ const bootstrap = ({ startScheduledWorker = true } = {}) => {
if (!bootstrapPromise) {
bootstrapPromise = (async () => {
await connectDb();
await ensureChatIndexes();
await ensureAvatarDefaults();
await configureSocketAdapter(global.io);
await Promise.all([
ensureChatIndexes(),
ensureAvatarDefaults(),
configureSocketAdapter(global.io),
getCallConfig(),
]);

logger.info('RUNTIME_READY', {
vercel: process.env.VERCEL === '1',
redis: Boolean(process.env.REDIS_URL),
realtimeWarm: true,
});

return true;
Expand Down
31 changes: 10 additions & 21 deletions backend/server/helpers/callState.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const { createClient } = require('redis');
const logger = require('./logger');
const {
getSocketRedisCommandClient,
isRedisConfigured,
} = require('./socketAdapter');

const DEFAULT_TTL_SEC = 24 * 60 * 60;
const MIN_TTL_SEC = 5 * 60;
Expand All @@ -12,10 +15,8 @@ const clampTtl = (value) => {
};

const stateTtlSec = clampTtl(process.env.CALL_STATE_TTL_SEC);
const redisUrl = String(process.env.REDIS_URL || '').trim();
const production = String(process.env.NODE_ENV || '').toLowerCase() === 'production';

let redisPromise = null;
let warnedFallback = false;
const memory = new Map();

Expand Down Expand Up @@ -60,7 +61,7 @@ const memoryDelete = (key) => {
};

const getRedis = async () => {
if (!redisUrl) {
if (!isRedisConfigured()) {
if (production) {
const error = new Error(
'REDIS_URL is required for durable production call state'
Expand All @@ -77,24 +78,12 @@ const getRedis = async () => {
return null;
}

if (redisPromise) return redisPromise;
const client = await getSocketRedisCommandClient();
if (client) return client;

redisPromise = (async () => {
const client = createClient({ url: redisUrl });
client.on('error', (error) => {
logger.error('CALL_STATE_REDIS_ERROR', { message: error.message });
});
await client.connect();
logger.info('CALL_STATE_REDIS_READY', {
ttlSec: stateTtlSec,
});
return client;
})().catch((error) => {
redisPromise = null;
throw error;
});

return redisPromise;
const error = new Error('Shared Redis runtime is not ready for call state');
error.code = 'CALL_STATE_REDIS_NOT_READY';
throw error;
};

const encodeState = (state, ttlSec = stateTtlSec) => ({
Expand Down
31 changes: 12 additions & 19 deletions backend/server/helpers/chatAbuse.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,26 @@
const crypto = require('crypto');
const { createClient } = require('redis');
const logger = require('./logger');
const {
getSocketRedisCommandClient,
isRedisConfigured,
} = require('./socketAdapter');

const localWindows = new Map();
let redisPromise = null;

const MAX_MESSAGES = Math.max(5, Number(process.env.CHAT_RATE_LIMIT_MESSAGES || 30));
const WINDOW_SECONDS = Math.max(5, Number(process.env.CHAT_RATE_LIMIT_WINDOW_SEC || 10));
const MAX_DUPLICATES = Math.max(2, Number(process.env.CHAT_DUPLICATE_LIMIT || 6));
const DUPLICATE_WINDOW_SECONDS = Math.max(10, Number(process.env.CHAT_DUPLICATE_WINDOW_SEC || 30));

const getRedis = async () => {
const url = String(process.env.REDIS_URL || '').trim();
if (!url) return null;
if (!redisPromise) {
redisPromise = (async () => {
const client = createClient({ url });
client.on('error', (error) => {
logger.warn('CHAT_ABUSE_REDIS_ERROR', { message: error.message });
});
await client.connect();
return client;
})().catch((error) => {
redisPromise = null;
logger.warn('CHAT_ABUSE_REDIS_CONNECT_FAILED', { message: error.message });
return null;
});
}
return redisPromise;
if (!isRedisConfigured()) return null;
const client = await getSocketRedisCommandClient();
if (client) return client;

logger.warn('CHAT_ABUSE_REDIS_NOT_READY', {
message: 'Shared Redis runtime is not ready; using local rate limits for this request',
});
return null;
};

const createRateError = (message, code) => {
Expand Down
72 changes: 39 additions & 33 deletions backend/server/helpers/chatReliability.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,32 @@ const wrapReliableChatInsert = (socket) => {
return;
}

// The warm Redis abuse guard must pass before any optional lookup starts.
// Sequence allocation and mention resolution can then run concurrently.
await assertChatSendAllowed({ userId: socket.userId, text: args.text || '' });
const sequence = await nextSequence(args.roomId);
const mentions = await resolveMentions({
text: args.text || '',
roomId: args.roomId,
roomType: args.roomType,
senderId: socket.userId,
});
const [sequence, mentions] = await Promise.all([
nextSequence(args.roomId),
resolveMentions({
text: args.text || '',
roomId: args.roomId,
roomType: args.roomType,
senderId: socket.userId,
}),
]);

const topicId = args.topicId || null;
const e2eeEnvelope = args.e2eeEnvelope && typeof args.e2eeEnvelope === 'object'
? args.e2eeEnvelope
: null;
const transcript = String(args.transcript || '').slice(0, 8000);

// Persist reliability metadata in the original create instead of doing a
// second database update after the message has already been written.
args.sequence = sequence;
args.mentionUserIds = mentions.mentionedUserIds;
args.topicId = topicId;
args.e2eeEnvelope = e2eeEnvelope;
args.transcript = transcript;

await original(args);

Expand All @@ -119,19 +137,21 @@ const wrapReliableChatInsert = (socket) => {

const inbox = await InboxModel.findOne({ where: { roomId: args.roomId } });
const ownerIds = asArray(toPlain(inbox)?.ownersId || args.ownersId);
const topicId = args.topicId || null;
const e2eeEnvelope = args.e2eeEnvelope && typeof args.e2eeEnvelope === 'object'
? args.e2eeEnvelope
: null;
const transcript = String(args.transcript || '').slice(0, 8000);

await created.update({
clientMessageId: args.clientMessageId,
sequence,
mentionUserIds: mentions.mentionedUserIds,
topicId,
e2eeEnvelope,
transcript,
// Ack as soon as the durable chat and room ownership are confirmed.
// Message-request and mention side effects should not hold the sender UI.
emitMeta({
socket,
chat: toPlain(created),
meta: {
clientMessageId: args.clientMessageId,
sequence,
mentionUserIds: mentions.mentionedUserIds,
topicId,
e2eeEnvelope,
transcript,
ownerIds,
},
});

if (args.roomType === 'private' && ownerIds.length === 2) {
Expand Down Expand Up @@ -163,20 +183,6 @@ const wrapReliableChatInsert = (socket) => {
});
});
}

emitMeta({
socket,
chat: toPlain(created),
meta: {
clientMessageId: args.clientMessageId,
sequence,
mentionUserIds: mentions.mentionedUserIds,
topicId,
e2eeEnvelope,
transcript,
ownerIds,
},
});
} catch (error0) {
logger.warn('CHAT_RELIABILITY_REJECTED', {
userId: socket.userId,
Expand Down
10 changes: 10 additions & 0 deletions backend/server/helpers/socketAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ const configureSocketAdapter = async (io = global.io) => {
return adapterPromise;
};

const getSocketRedisCommandClient = async () => {
if (!isRedisConfigured()) return null;
if (redisClients?.pubClient?.isReady) return redisClients.pubClient;
if (!adapterPromise) return null;

const clients = await adapterPromise;
return clients?.pubClient?.isReady ? clients.pubClient : null;
};

const closeSocketAdapter = async () => {
const clients = redisClients;
redisClients = null;
Expand All @@ -58,6 +67,7 @@ const closeSocketAdapter = async () => {

module.exports = {
configureSocketAdapter,
getSocketRedisCommandClient,
closeSocketAdapter,
isRedisConfigured,
};
Loading
Loading