snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
7
apps/meteor/server/startup/appcache.ts
Normal file
7
apps/meteor/server/startup/appcache.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
if (Meteor.AppCache) {
|
||||
Meteor.AppCache.config({
|
||||
onlineOnly: ['/elements/', '/landing/', '/moment-locales/', '/scripts/'],
|
||||
});
|
||||
}
|
||||
240
apps/meteor/server/startup/callHistoryTestData.ts
Normal file
240
apps/meteor/server/startup/callHistoryTestData.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { CallHistory, MediaCalls } from '@rocket.chat/models';
|
||||
|
||||
export async function addCallHistoryTestData(uid: string, extraUid: string): Promise<void> {
|
||||
const callId1 = 'rocketchat.internal.call.test';
|
||||
const callId2 = 'rocketchat.internal.call.test.2';
|
||||
const callId3 = 'rocketchat.external.call.test.outbound';
|
||||
const callId4 = 'rocketchat.external.call.test.inbound';
|
||||
|
||||
const extraCallId1 = 'rocketchat.extra.call.test.1';
|
||||
const extraCallId2 = 'rocketchat.extra.call.test.2';
|
||||
|
||||
await CallHistory.deleteMany({ uid });
|
||||
await MediaCalls.deleteMany({ _id: { $in: [callId1, callId2, callId3, callId4] } });
|
||||
|
||||
await CallHistory.insertMany([
|
||||
{
|
||||
_id: 'rocketchat.internal.history.test.outbound',
|
||||
ts: new Date(),
|
||||
callId: callId1,
|
||||
state: 'ended',
|
||||
type: 'media-call',
|
||||
duration: 10,
|
||||
endedAt: new Date(),
|
||||
external: false,
|
||||
uid,
|
||||
contactId: extraUid,
|
||||
direction: 'outbound',
|
||||
contactName: 'Pineapple', // random words used for searching
|
||||
contactUsername: 'fruit-001',
|
||||
},
|
||||
{
|
||||
_id: 'rocketchat.internal.history.test.inbound',
|
||||
ts: new Date(),
|
||||
callId: callId2,
|
||||
state: 'not-answered',
|
||||
type: 'media-call',
|
||||
duration: 10,
|
||||
endedAt: new Date(),
|
||||
external: false,
|
||||
uid,
|
||||
contactId: extraUid,
|
||||
direction: 'inbound',
|
||||
contactName: 'Apple',
|
||||
contactUsername: 'fruit-002',
|
||||
},
|
||||
{
|
||||
_id: 'rocketchat.internal.history.test.outbound.2',
|
||||
ts: new Date(),
|
||||
callId: extraCallId1,
|
||||
state: 'transferred',
|
||||
type: 'media-call',
|
||||
duration: 10,
|
||||
endedAt: new Date(),
|
||||
external: false,
|
||||
uid,
|
||||
contactId: extraUid,
|
||||
direction: 'outbound',
|
||||
contactName: 'Grapefruit 002',
|
||||
contactUsername: 'username-001',
|
||||
},
|
||||
{
|
||||
_id: 'rocketchat.internal.history.test.inbound.2',
|
||||
ts: new Date(),
|
||||
callId: extraCallId2,
|
||||
state: 'transferred',
|
||||
type: 'media-call',
|
||||
duration: 10,
|
||||
endedAt: new Date(),
|
||||
external: false,
|
||||
uid,
|
||||
contactId: extraUid,
|
||||
direction: 'inbound',
|
||||
contactName: 'Pasta 1',
|
||||
contactUsername: 'meal',
|
||||
},
|
||||
{
|
||||
_id: 'rocketchat.external.history.test.outbound',
|
||||
ts: new Date(),
|
||||
callId: callId3,
|
||||
state: 'failed',
|
||||
type: 'media-call',
|
||||
duration: 10,
|
||||
endedAt: new Date(),
|
||||
external: true,
|
||||
uid,
|
||||
direction: 'outbound',
|
||||
contactExtension: '1001',
|
||||
},
|
||||
{
|
||||
_id: 'rocketchat.external.history.test.inbound',
|
||||
ts: new Date(),
|
||||
callId: callId4,
|
||||
state: 'ended',
|
||||
type: 'media-call',
|
||||
duration: 10,
|
||||
endedAt: new Date(),
|
||||
external: true,
|
||||
uid,
|
||||
direction: 'inbound',
|
||||
contactExtension: '1002',
|
||||
},
|
||||
]);
|
||||
|
||||
await MediaCalls.insertMany([
|
||||
{
|
||||
_id: callId1,
|
||||
service: 'webrtc',
|
||||
kind: 'direct',
|
||||
state: 'hangup',
|
||||
createdBy: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
caller: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
contractId: 'contract1',
|
||||
},
|
||||
callee: {
|
||||
type: 'user',
|
||||
id: extraUid,
|
||||
contractId: 'contract2',
|
||||
},
|
||||
ended: true,
|
||||
endedBy: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
},
|
||||
endedAt: new Date(),
|
||||
hangupReason: 'normal',
|
||||
expiresAt: new Date(),
|
||||
acceptedAt: new Date(),
|
||||
activatedAt: new Date(),
|
||||
uids: [uid, extraUid],
|
||||
features: ['audio'],
|
||||
},
|
||||
{
|
||||
_id: callId2,
|
||||
service: 'webrtc',
|
||||
kind: 'direct',
|
||||
state: 'hangup',
|
||||
createdBy: {
|
||||
type: 'user',
|
||||
id: extraUid,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
caller: {
|
||||
type: 'user',
|
||||
id: extraUid,
|
||||
contractId: 'contract1',
|
||||
},
|
||||
callee: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
contractId: 'contract2',
|
||||
},
|
||||
ended: true,
|
||||
endedBy: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
},
|
||||
endedAt: new Date(),
|
||||
hangupReason: 'normal',
|
||||
expiresAt: new Date(),
|
||||
acceptedAt: new Date(),
|
||||
activatedAt: new Date(),
|
||||
uids: [uid, extraUid],
|
||||
features: ['audio'],
|
||||
},
|
||||
{
|
||||
_id: callId3,
|
||||
service: 'webrtc',
|
||||
kind: 'direct',
|
||||
state: 'hangup',
|
||||
createdBy: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
},
|
||||
createdAt: new Date(),
|
||||
caller: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
contractId: 'contract1',
|
||||
},
|
||||
callee: {
|
||||
type: 'sip',
|
||||
id: '1001',
|
||||
contractId: 'contract2',
|
||||
},
|
||||
ended: true,
|
||||
endedBy: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
},
|
||||
endedAt: new Date(),
|
||||
hangupReason: 'normal',
|
||||
expiresAt: new Date(),
|
||||
acceptedAt: new Date(),
|
||||
activatedAt: new Date(),
|
||||
uids: [uid],
|
||||
features: ['audio'],
|
||||
sipCallId: 'sipCallId3',
|
||||
},
|
||||
{
|
||||
_id: callId4,
|
||||
service: 'webrtc',
|
||||
kind: 'direct',
|
||||
state: 'hangup',
|
||||
createdBy: {
|
||||
type: 'sip',
|
||||
id: '1001',
|
||||
},
|
||||
createdAt: new Date(),
|
||||
caller: {
|
||||
type: 'sip',
|
||||
id: '1001',
|
||||
contractId: 'contract1',
|
||||
},
|
||||
callee: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
contractId: 'contract2',
|
||||
},
|
||||
ended: true,
|
||||
endedBy: {
|
||||
type: 'user',
|
||||
id: uid,
|
||||
},
|
||||
endedAt: new Date(),
|
||||
hangupReason: 'normal',
|
||||
expiresAt: new Date(),
|
||||
acceptedAt: new Date(),
|
||||
activatedAt: new Date(),
|
||||
uids: [uid],
|
||||
features: ['audio'],
|
||||
sipCallId: 'sipCallId4',
|
||||
},
|
||||
]);
|
||||
}
|
||||
38
apps/meteor/server/startup/callbacks.ts
Normal file
38
apps/meteor/server/startup/callbacks.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { performance } from 'universal-perf-hooks';
|
||||
|
||||
import { callbacks } from '../lib/callbacks';
|
||||
import { metrics, StatsTracker } from '../lib/metrics';
|
||||
|
||||
callbacks.setLogger(new Logger('Callbacks'));
|
||||
|
||||
callbacks.setMetricsTrackers({
|
||||
trackCallback: ({ hook, id }) => {
|
||||
const start = performance.now();
|
||||
|
||||
const stopTimer = metrics.rocketchatCallbacks.startTimer({ hook, callback: id });
|
||||
const stopHistogram = metrics.rocketchatCallbacksSeconds.startTimer({ hook, callback: id });
|
||||
|
||||
return (): void => {
|
||||
const end = performance.now();
|
||||
StatsTracker.timing('callbacks.time', end - start, [`hook:${hook}`, `callback:${id}`]);
|
||||
|
||||
stopTimer();
|
||||
stopHistogram();
|
||||
};
|
||||
},
|
||||
trackHook: ({ hook, length }) => {
|
||||
const stopTimer = metrics.rocketchatHooks.startTimer({
|
||||
hook,
|
||||
callbacks_length: length,
|
||||
});
|
||||
const stopHistogram = metrics.rocketchatHooksSeconds.startTimer({
|
||||
hook,
|
||||
callbacks_length: length,
|
||||
});
|
||||
return (): void => {
|
||||
stopTimer();
|
||||
stopHistogram();
|
||||
};
|
||||
},
|
||||
});
|
||||
26
apps/meteor/server/startup/cloudRegistration.ts
Normal file
26
apps/meteor/server/startup/cloudRegistration.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
export async function ensureCloudWorkspaceRegistered(): Promise<void> {
|
||||
const cloudWorkspaceClientId = await Settings.getValueById('Cloud_Workspace_Client_Id');
|
||||
const cloudWorkspaceClientSecret = await Settings.getValueById('Cloud_Workspace_Client_Secret');
|
||||
const showSetupWizard = await Settings.findOneById('Show_Setup_Wizard', {
|
||||
projection: { value: 1, valueSource: 1, processEnvValue: 1 },
|
||||
});
|
||||
|
||||
// skip if both fields are already set, which means the workspace is already registered
|
||||
if (!!cloudWorkspaceClientId && !!cloudWorkspaceClientSecret) {
|
||||
return;
|
||||
}
|
||||
|
||||
// skip if the setup wizard still not completed
|
||||
if (showSetupWizard?.value !== 'completed') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (showSetupWizard.valueSource === 'processEnvValue' && showSetupWizard.value === showSetupWizard.processEnvValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, set the setup wizard to in_progress forcing admins to complete the registration
|
||||
await Settings.updateValueById('Show_Setup_Wizard', 'in_progress');
|
||||
}
|
||||
14
apps/meteor/server/startup/coreApps.ts
Normal file
14
apps/meteor/server/startup/coreApps.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BannerModule } from '../modules/core-apps/banner.module';
|
||||
import { CloudAnnouncementsModule } from '../modules/core-apps/cloudAnnouncements.module';
|
||||
import { CloudSubscriptionCommunication } from '../modules/core-apps/cloudSubscriptionCommunication.module';
|
||||
import { MentionModule } from '../modules/core-apps/mention.module';
|
||||
import { Nps } from '../modules/core-apps/nps.module';
|
||||
import { VideoConfModule } from '../modules/core-apps/videoconf.module';
|
||||
import { registerCoreApp } from '../services/uikit-core-app/service';
|
||||
|
||||
registerCoreApp(new CloudAnnouncementsModule());
|
||||
registerCoreApp(new CloudSubscriptionCommunication());
|
||||
registerCoreApp(new Nps());
|
||||
registerCoreApp(new BannerModule());
|
||||
registerCoreApp(new VideoConfModule());
|
||||
registerCoreApp(new MentionModule());
|
||||
16
apps/meteor/server/startup/cron.ts
Normal file
16
apps/meteor/server/startup/cron.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
|
||||
import { npsCron } from '../cron/nps';
|
||||
import { oembedCron } from '../cron/oembed';
|
||||
import { startCron } from '../cron/start';
|
||||
import { temporaryUploadCleanupCron } from '../cron/temporaryUploadsCleanup';
|
||||
import { usageReportCron } from '../cron/usageReport';
|
||||
import { userDataDownloadsCron } from '../cron/userDataDownloads';
|
||||
import { videoConferencesCron } from '../cron/videoConferences';
|
||||
|
||||
const logger = new Logger('SyncedCron');
|
||||
|
||||
export const startCronJobs = async (): Promise<void> => {
|
||||
await Promise.all([startCron(), oembedCron(), usageReportCron(logger), npsCron(), temporaryUploadCleanupCron(), videoConferencesCron()]);
|
||||
userDataDownloadsCron();
|
||||
};
|
||||
87
apps/meteor/server/startup/ensureMessagesTextIndex.ts
Normal file
87
apps/meteor/server/startup/ensureMessagesTextIndex.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
|
||||
import { SystemLogger } from '../lib/logger/system';
|
||||
|
||||
const { USE_ROOM_SEARCH_INDEX = 'false' } = process.env;
|
||||
|
||||
// MongoDB stores a text index's key with `_fts: 'text'` / `_ftsx: 1` placeholders
|
||||
// and tracks the original text fields in `weights`. Classify by looking at the
|
||||
// non-placeholder prefix fields plus weights.
|
||||
const classifyTextIndex = (idx: { key: Record<string, unknown>; weights?: Record<string, number> }) => {
|
||||
const { weights, key } = idx;
|
||||
if (weights?.msg !== 1 || Object.keys(weights).length !== 1) {
|
||||
return 'other';
|
||||
}
|
||||
|
||||
const prefix = Object.keys(key).filter((k) => k !== '_fts' && k !== '_ftsx');
|
||||
if (prefix.length === 0) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if (prefix.length === 1 && prefix[0] === 'rid' && key.rid === 1) {
|
||||
return 'room-scoped';
|
||||
}
|
||||
|
||||
return 'other';
|
||||
};
|
||||
|
||||
export const ensureMessagesTextIndex = async (): Promise<void> => {
|
||||
const desiredShape = USE_ROOM_SEARCH_INDEX === 'true' ? 'room-scoped' : 'default';
|
||||
|
||||
SystemLogger.debug({
|
||||
msg: 'checking messages text index',
|
||||
USE_ROOM_SEARCH_INDEX,
|
||||
desired: desiredShape,
|
||||
});
|
||||
|
||||
let existing;
|
||||
try {
|
||||
existing = await Messages.col.indexes();
|
||||
} catch (err) {
|
||||
SystemLogger.error({ msg: 'failed to list messages indexes; skipping text index check', err });
|
||||
return;
|
||||
}
|
||||
|
||||
const textIndexes = existing.filter((idx) => Object.values(idx.key).includes('text'));
|
||||
const matching = textIndexes.find((idx) => classifyTextIndex(idx) === desiredShape);
|
||||
const stale = textIndexes.filter((idx) => idx !== matching);
|
||||
|
||||
if (matching && stale.length === 0) {
|
||||
SystemLogger.debug({ msg: 'messages text index already matches desired shape', name: matching.name });
|
||||
return;
|
||||
}
|
||||
|
||||
for (const idx of stale) {
|
||||
if (!idx.name) {
|
||||
continue;
|
||||
}
|
||||
SystemLogger.startup({
|
||||
msg: 'dropping messages text index; may take several minutes on large databases',
|
||||
name: idx.name,
|
||||
shape: classifyTextIndex(idx),
|
||||
});
|
||||
try {
|
||||
await Messages.col.dropIndex(idx.name);
|
||||
SystemLogger.startup({ msg: 'dropped messages text index', name: idx.name });
|
||||
} catch (err) {
|
||||
SystemLogger.error({ msg: 'failed to drop messages text index; aborting', name: idx.name, err });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (matching) {
|
||||
SystemLogger.debug({ msg: 'messages text index already matches desired shape', name: matching.name });
|
||||
return;
|
||||
}
|
||||
|
||||
SystemLogger.startup({
|
||||
msg: 'creating messages text index; may take several minutes on large databases',
|
||||
shape: desiredShape,
|
||||
});
|
||||
try {
|
||||
const name = await Messages.col.createIndex(desiredShape === 'room-scoped' ? { rid: 1, msg: 'text' } : { msg: 'text' });
|
||||
SystemLogger.startup({ msg: 'created messages text index', name, shape: desiredShape });
|
||||
} catch (err) {
|
||||
SystemLogger.error({ msg: 'failed to create messages text index', shape: desiredShape, err });
|
||||
}
|
||||
};
|
||||
8
apps/meteor/server/startup/generateKeys.ts
Normal file
8
apps/meteor/server/startup/generateKeys.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { FederationKeys } from '@rocket.chat/models';
|
||||
|
||||
// Create key pair if needed
|
||||
export async function generateFederationKeys() {
|
||||
if (!(await FederationKeys.getPublicKey())) {
|
||||
await FederationKeys.generateKeys();
|
||||
}
|
||||
}
|
||||
17
apps/meteor/server/startup/httpSocketTimeout.ts
Normal file
17
apps/meteor/server/startup/httpSocketTimeout.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
|
||||
// meteor's webapp arms a 5s idle-socket timeout (and re-arms it after every
|
||||
// response), which kills keep-alive conns a fronting proxy may be about to
|
||||
// reuse — the root of the CI dynamic-import truncation flake (PR #41399).
|
||||
// Overriding it is opt-in: only CI sets HTTP_SOCKET_TIMEOUT_MS, pointing it
|
||||
// above traefik's idleConnTimeout so the proxy always closes conns first.
|
||||
const timeout = parseInt(process.env.HTTP_SOCKET_TIMEOUT_MS ?? '', 10);
|
||||
if (timeout > 0) {
|
||||
const { httpServer, _timeoutAdjustmentRequestCallback } = WebApp as typeof WebApp & {
|
||||
_timeoutAdjustmentRequestCallback: (req: unknown, res: unknown) => void;
|
||||
};
|
||||
httpServer.removeListener('request', _timeoutAdjustmentRequestCallback);
|
||||
httpServer.setTimeout(timeout);
|
||||
httpServer.keepAliveTimeout = timeout;
|
||||
httpServer.headersTimeout = timeout + 1000;
|
||||
}
|
||||
29
apps/meteor/server/startup/index.ts
Normal file
29
apps/meteor/server/startup/index.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import './appcache';
|
||||
import './callbacks';
|
||||
import { ensureMessagesTextIndex } from './ensureMessagesTextIndex';
|
||||
import './initialData';
|
||||
import './serverRunning';
|
||||
import './coreApps';
|
||||
import { generateFederationKeys } from './generateKeys';
|
||||
import './presenceTroubleshoot';
|
||||
import './httpSocketTimeout';
|
||||
import '../hooks';
|
||||
import '../lib/rooms/roomTypes';
|
||||
import '../lib/settingsRegenerator';
|
||||
import { performMigrationProcedure } from './migrations';
|
||||
import { isRunningMs } from '../lib/isRunningMs';
|
||||
|
||||
export const startup = async () => {
|
||||
await performMigrationProcedure();
|
||||
|
||||
await generateFederationKeys();
|
||||
|
||||
// NOTE: cron jobs are started from main.ts after startRocketChat(), so jobs
|
||||
// that contact Rocket.Chat Cloud on boot (e.g. the usage report) run only
|
||||
// after the license — including the offline flag — has been applied.
|
||||
setImmediate(() => ensureMessagesTextIndex());
|
||||
// only starts network broker if running in micro services mode
|
||||
if (!isRunningMs()) {
|
||||
require('./localServices');
|
||||
}
|
||||
};
|
||||
257
apps/meteor/server/startup/initialData.ts
Normal file
257
apps/meteor/server/startup/initialData.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { UserStatus, type IUser } from '@rocket.chat/core-typings';
|
||||
import { Settings, Rooms, Users, Roles } from '@rocket.chat/models';
|
||||
import { validateEmail } from '@rocket.chat/tools';
|
||||
import colors from 'colors/safe';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { addCallHistoryTestData } from './callHistoryTestData';
|
||||
import { FileUpload } from '../lib/media/file-upload';
|
||||
import { notifyOnSettingChangedById } from '../lib/notifyListener';
|
||||
import { addUserRolesAsync } from '../lib/roles/addUserRoles';
|
||||
import { addUserToDefaultChannels } from '../lib/rooms/addUserToDefaultChannels';
|
||||
import { checkUsernameAvailability } from '../lib/users/checkUsernameAvailability';
|
||||
import { settings } from '../settings';
|
||||
|
||||
export async function insertAdminUserFromEnv() {
|
||||
if (process.env.ADMIN_PASS) {
|
||||
if ((await Roles.countUsersInRole('admin')) === 0) {
|
||||
const adminUser: Partial<IUser> = {
|
||||
name: process.env.ADMIN_NAME || 'Administrator',
|
||||
username: 'admin',
|
||||
status: UserStatus.OFFLINE,
|
||||
statusDefault: UserStatus.ONLINE,
|
||||
utcOffset: 0,
|
||||
active: true,
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
console.log(colors.green(`Name: ${adminUser.name}`));
|
||||
|
||||
if (process.env.ADMIN_EMAIL) {
|
||||
if (validateEmail(process.env.ADMIN_EMAIL)) {
|
||||
if (!(await Users.findOneByEmailAddress(process.env.ADMIN_EMAIL))) {
|
||||
adminUser.emails = [
|
||||
{
|
||||
address: process.env.ADMIN_EMAIL,
|
||||
verified: process.env.ADMIN_EMAIL_VERIFIED === 'true',
|
||||
},
|
||||
];
|
||||
|
||||
console.log(colors.green(`Email: ${process.env.ADMIN_EMAIL}`));
|
||||
} else {
|
||||
console.log(colors.red('Email provided already exists; Ignoring environment variables ADMIN_EMAIL'));
|
||||
}
|
||||
} else {
|
||||
console.log(colors.red('Email provided is invalid; Ignoring environment variables ADMIN_EMAIL'));
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.ADMIN_USERNAME) {
|
||||
let nameValidation;
|
||||
|
||||
try {
|
||||
nameValidation = new RegExp(`^${settings.get('UTF8_User_Names_Validation')}$`);
|
||||
} catch (error) {
|
||||
nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
|
||||
}
|
||||
|
||||
if (nameValidation.test(process.env.ADMIN_USERNAME)) {
|
||||
try {
|
||||
await checkUsernameAvailability(process.env.ADMIN_USERNAME);
|
||||
adminUser.username = process.env.ADMIN_USERNAME;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
colors.red('Username provided already exists or is blocked from usage; Ignoring environment variables ADMIN_USERNAME'),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.log(colors.red('Username provided is invalid; Ignoring environment variables ADMIN_USERNAME'));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(colors.green(`Username: ${adminUser.username}`));
|
||||
|
||||
const { insertedId: userId } = await Users.create(adminUser);
|
||||
|
||||
await Accounts.setPasswordAsync(userId, process.env.ADMIN_PASS);
|
||||
|
||||
await addUserRolesAsync(userId, ['admin']);
|
||||
} else {
|
||||
console.log(colors.red('Users with admin role already exist; Ignoring environment variables ADMIN_PASS'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.startup(async () => {
|
||||
const dynamicImport = {
|
||||
'dynamic-import': {
|
||||
useLocationOrigin: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (!Meteor.settings) {
|
||||
Meteor.settings = {
|
||||
public: {
|
||||
packages: {
|
||||
'dynamic-import': dynamicImport,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!Meteor.settings.public) {
|
||||
Meteor.settings.public = {
|
||||
packages: {
|
||||
'dynamic-import': dynamicImport,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!Meteor.settings.public.packages) {
|
||||
Meteor.settings.public.packages = dynamicImport;
|
||||
}
|
||||
|
||||
Meteor.settings.public.packages['dynamic-import'] = dynamicImport['dynamic-import'];
|
||||
|
||||
if (!settings.get('Initial_Channel_Created')) {
|
||||
const exists = await Rooms.findOneById('GENERAL', { projection: { _id: 1 } });
|
||||
if (!exists) {
|
||||
await Rooms.createWithIdTypeAndName('GENERAL', 'c', 'general', {
|
||||
default: true,
|
||||
});
|
||||
}
|
||||
|
||||
(await Settings.updateValueById('Initial_Channel_Created', true)).modifiedCount &&
|
||||
void notifyOnSettingChangedById('Initial_Channel_Created');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!(await Users.findOneById('rocket.cat', { projection: { _id: 1 } }))) {
|
||||
await Users.create({
|
||||
_id: 'rocket.cat',
|
||||
name: 'Rocket.Cat',
|
||||
username: 'rocket.cat',
|
||||
status: UserStatus.ONLINE,
|
||||
statusDefault: UserStatus.ONLINE,
|
||||
utcOffset: 0,
|
||||
active: true,
|
||||
type: 'bot',
|
||||
});
|
||||
|
||||
await addUserRolesAsync('rocket.cat', ['bot']);
|
||||
|
||||
const asset = await Assets.getBinaryAsync('avatars/rocketcat.png');
|
||||
if (asset) {
|
||||
const buffer = Buffer.from(asset);
|
||||
|
||||
const fileStore = FileUpload.getStore('Avatars');
|
||||
await fileStore.deleteByName('rocket.cat');
|
||||
|
||||
const file = {
|
||||
userId: 'rocket.cat',
|
||||
type: 'image/png',
|
||||
size: buffer.length,
|
||||
};
|
||||
|
||||
const upload = await fileStore.insert(file, buffer);
|
||||
await Users.setAvatarData('rocket.cat', 'local', upload.etag);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Error creating default `rocket.cat` user, if you created a user with this username please remove it and restart the server',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await insertAdminUserFromEnv();
|
||||
|
||||
if (typeof process.env.INITIAL_USER === 'string' && process.env.INITIAL_USER.length > 0) {
|
||||
try {
|
||||
const initialUser = JSON.parse(process.env.INITIAL_USER);
|
||||
|
||||
if (!initialUser._id) {
|
||||
console.log(colors.red('No _id provided; Ignoring environment variable INITIAL_USER'));
|
||||
} else if (!(await Users.findOneById(initialUser._id))) {
|
||||
console.log(colors.green('Inserting initial user:'));
|
||||
console.log(colors.green(JSON.stringify(initialUser, null, 2)));
|
||||
await Users.create(initialUser);
|
||||
|
||||
await addUserToDefaultChannels(initialUser, true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(colors.red('Error processing environment variable INITIAL_USER'), e);
|
||||
}
|
||||
}
|
||||
|
||||
if ((await Roles.countUsersInRole('admin')) === 0) {
|
||||
const oldestUser = await Users.getOldest({ projection: { _id: 1, username: 1, name: 1 } });
|
||||
|
||||
if (oldestUser) {
|
||||
await addUserRolesAsync(oldestUser._id, ['admin']);
|
||||
console.log(`No admins are found. Set ${oldestUser.username || oldestUser.name} as admin for being the oldest user`);
|
||||
}
|
||||
}
|
||||
|
||||
if ((await Roles.countUsersInRole('admin')) !== 0) {
|
||||
if (settings.get('Show_Setup_Wizard') === 'pending') {
|
||||
console.log('Setting Setup Wizard to "in_progress" because, at least, one admin was found');
|
||||
|
||||
(await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount &&
|
||||
void notifyOnSettingChangedById('Show_Setup_Wizard');
|
||||
}
|
||||
}
|
||||
|
||||
await Users.removeById('rocketchat.internal.admin.test');
|
||||
|
||||
if (process.env.TEST_MODE === 'true' || process.env.TEST_MODE === 'api') {
|
||||
console.log(colors.green('Inserting admin test user:'));
|
||||
|
||||
const adminUser: Omit<IUser, 'createdAt' | 'roles' | '_updatedAt'> = {
|
||||
_id: 'rocketchat.internal.admin.test',
|
||||
name: 'RocketChat Internal Admin Test',
|
||||
username: 'rocketchat.internal.admin.test',
|
||||
emails: [
|
||||
{
|
||||
address: 'rocketchat.internal.admin.test@rocket.chat',
|
||||
verified: true,
|
||||
},
|
||||
],
|
||||
status: UserStatus.OFFLINE,
|
||||
statusDefault: UserStatus.ONLINE,
|
||||
utcOffset: 0,
|
||||
active: true,
|
||||
type: 'user',
|
||||
};
|
||||
|
||||
console.log(colors.green(`Name: ${adminUser.name}`));
|
||||
console.log(colors.green(`Email: ${adminUser.emails![0].address}`));
|
||||
console.log(colors.green(`Username: ${adminUser.username}`));
|
||||
console.log(colors.green(`Password: ${adminUser._id}`));
|
||||
|
||||
if (await Users.findOneByEmailAddress(adminUser.emails![0].address)) {
|
||||
throw new Meteor.Error(`Email ${adminUser.emails![0].address} already exists`, "Rocket.Chat can't run in test mode");
|
||||
}
|
||||
|
||||
if (!(await checkUsernameAvailability(adminUser.username!))) {
|
||||
throw new Meteor.Error(`Username ${adminUser.username} already exists`, "Rocket.Chat can't run in test mode");
|
||||
}
|
||||
|
||||
await Users.create(adminUser);
|
||||
|
||||
await Accounts.setPasswordAsync(adminUser._id, adminUser._id);
|
||||
|
||||
await addUserRolesAsync(adminUser._id, ['admin']);
|
||||
|
||||
if (settings.get('Show_Setup_Wizard') === 'pending') {
|
||||
(await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount &&
|
||||
void notifyOnSettingChangedById('Show_Setup_Wizard');
|
||||
}
|
||||
|
||||
await addUserToDefaultChannels(adminUser as IUser, true);
|
||||
|
||||
// Create sample call history for API tests
|
||||
return addCallHistoryTestData('rocketchat.internal.admin.test', 'rocket.cat');
|
||||
}
|
||||
});
|
||||
12
apps/meteor/server/startup/localServices.ts
Normal file
12
apps/meteor/server/startup/localServices.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { api, LocalBroker } from '@rocket.chat/core-services';
|
||||
|
||||
import { StreamerCentral } from '../modules/streamer/streamer.module';
|
||||
|
||||
const broker = new LocalBroker();
|
||||
|
||||
broker.onBroadcast((eventName: string, args: unknown[]) => {
|
||||
StreamerCentral.emit('broadcast', 'local', 'broadcast', [{ eventName, args }]);
|
||||
});
|
||||
|
||||
api.setBroker(broker);
|
||||
void api.start();
|
||||
45
apps/meteor/server/startup/migrations/index.ts
Normal file
45
apps/meteor/server/startup/migrations/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import './minimumVersion';
|
||||
import './v293';
|
||||
import './v294';
|
||||
import './v295';
|
||||
import './v296';
|
||||
import './v297';
|
||||
import './v298';
|
||||
import './v299';
|
||||
import './v300';
|
||||
import './v301';
|
||||
import './v303';
|
||||
import './v304';
|
||||
import './v305';
|
||||
import './v306';
|
||||
import './v307';
|
||||
import './v308';
|
||||
import './v309';
|
||||
import './v310';
|
||||
import './v311';
|
||||
import './v312';
|
||||
import './v313';
|
||||
import './v314';
|
||||
import './v315';
|
||||
import './v316';
|
||||
import './v317';
|
||||
import './v318';
|
||||
import './v319';
|
||||
import './v320';
|
||||
import './v321';
|
||||
import './v322';
|
||||
import './v323';
|
||||
import './v324';
|
||||
import './v325';
|
||||
import './v326';
|
||||
import './v327';
|
||||
import './v328';
|
||||
import './v329';
|
||||
import './v330';
|
||||
import './v331';
|
||||
import './v332';
|
||||
import './v333';
|
||||
import './v334';
|
||||
import './v335';
|
||||
|
||||
export * from './xrun';
|
||||
20
apps/meteor/server/startup/migrations/minimumVersion.ts
Normal file
20
apps/meteor/server/startup/migrations/minimumVersion.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
// this was the first migration added on version 6.0, so if this needs to run,
|
||||
// it means the server was never updated to 6.x, which is not supported.
|
||||
addMigration({
|
||||
version: 292,
|
||||
up() {
|
||||
throw new Error(
|
||||
[
|
||||
'UPGRADE NOT SUPPORTED!',
|
||||
'',
|
||||
`It seems you're trying to upgrade from an unsupported version!`,
|
||||
'',
|
||||
'To be able to update to version 7.x.y you need to update to version 6.x first.',
|
||||
'',
|
||||
'Read more: https://go.rocket.chat/i/how-to-upgrade',
|
||||
].join('\n'),
|
||||
);
|
||||
},
|
||||
});
|
||||
9
apps/meteor/server/startup/migrations/v293.ts
Normal file
9
apps/meteor/server/startup/migrations/v293.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { upsertPermissions } from '../../lib/authorization/upsertPermissions';
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 293,
|
||||
async up() {
|
||||
await upsertPermissions();
|
||||
},
|
||||
});
|
||||
37
apps/meteor/server/startup/migrations/v294.ts
Normal file
37
apps/meteor/server/startup/migrations/v294.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Apps } from '@rocket.chat/apps';
|
||||
import type { IAppStorageItem } from '@rocket.chat/apps/dist/server/storage/IAppStorageItem';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 294,
|
||||
async up() {
|
||||
if (!Apps.self) {
|
||||
throw new Error('Apps Orchestrator not registered.');
|
||||
}
|
||||
|
||||
Apps.initialize();
|
||||
|
||||
const sigMan = Apps.getManager().getSignatureManager();
|
||||
const appsStorage = Apps.getStorage();
|
||||
|
||||
const apps = await appsStorage.retrieveAll();
|
||||
|
||||
for (const app of apps.values()) {
|
||||
if (app.installationSource && app.signature) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedApp = {
|
||||
...app,
|
||||
migrated: true,
|
||||
installationSource: 'marketplaceInfo' in app ? 'marketplace' : 'private',
|
||||
} as IAppStorageItem;
|
||||
|
||||
await appsStorage.updatePartialAndReturnDocument({
|
||||
...updatedApp,
|
||||
signature: await sigMan.signApp(updatedApp),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
45
apps/meteor/server/startup/migrations/v295.ts
Normal file
45
apps/meteor/server/startup/migrations/v295.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { ISetting } from '@rocket.chat/core-typings';
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
import { updateAuditedBySystem } from '../../settings/lib/auditedSettingUpdates';
|
||||
|
||||
addMigration({
|
||||
version: 295,
|
||||
name: 'Change old "LDAP_Background_Sync_Interval" and "CROWD_Sync_Interval" to a pre-defined values instead of accept any input from the user',
|
||||
async up() {
|
||||
const oldLdapDefault = 'Every 24 hours';
|
||||
const oldCrowdDefault = 'Every 60 mins';
|
||||
|
||||
const newLdapDefault = 'every_24_hours';
|
||||
const newCrowdDefault = 'every_1_hours';
|
||||
|
||||
const ldapSyncInterval = await Settings.findOneById<Pick<ISetting, 'value'>>('LDAP_Background_Sync_Interval', {
|
||||
projection: { value: 1 },
|
||||
});
|
||||
const crowdSyncInterval = await Settings.findOneById<Pick<ISetting, 'value'>>('CROWD_Sync_Interval', { projection: { value: 1 } });
|
||||
|
||||
// update setting values
|
||||
await updateAuditedBySystem({
|
||||
reason: 'Migration 295',
|
||||
})(Settings.updateValueById, 'LDAP_Background_Sync_Interval', newLdapDefault);
|
||||
await updateAuditedBySystem({
|
||||
reason: 'Migration 295',
|
||||
})(Settings.updateValueById, 'CROWD_Sync_Interval', newCrowdDefault);
|
||||
|
||||
// notify user about the changes if the value was different from the default
|
||||
|
||||
if (ldapSyncInterval && ldapSyncInterval.value !== oldLdapDefault) {
|
||||
SystemLogger.warn(
|
||||
`The value of the setting 'LDAP background synchronization interval' has changed from "${ldapSyncInterval.value}" to "${newLdapDefault}". Please review your settings.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (crowdSyncInterval && crowdSyncInterval.value !== oldCrowdDefault) {
|
||||
SystemLogger.warn(
|
||||
`The value of the setting 'CROWD background synchronization interval' has changed from "${crowdSyncInterval.value}" to "${newCrowdDefault}". Please review your settings.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v296.ts
Normal file
22
apps/meteor/server/startup/migrations/v296.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
addMigration({
|
||||
version: 296,
|
||||
name: 'Reset the default value of Login Terms setting and replace by empty string',
|
||||
async up() {
|
||||
const oldLoginTermsValue =
|
||||
'By proceeding you are agreeing to our <a href="terms-of-service">Terms of Service</a>, <a href="privacy-policy">Privacy Policy</a> and <a href="legal-notice">Legal Notice</a>.';
|
||||
|
||||
const loginTermsValue = settings.get('Layout_Login_Terms');
|
||||
|
||||
if (loginTermsValue === oldLoginTermsValue) {
|
||||
// TODO: audit
|
||||
await Settings.updateOne({ _id: 'Layout_Login_Terms' }, { $set: { value: '', packageValue: '' } });
|
||||
SystemLogger.warn(`The default value of the setting 'Login Terms' has changed to an empty string. Please review your settings.`);
|
||||
}
|
||||
},
|
||||
});
|
||||
9
apps/meteor/server/startup/migrations/v297.ts
Normal file
9
apps/meteor/server/startup/migrations/v297.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { upsertPermissions } from '../../lib/authorization/upsertPermissions';
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 297,
|
||||
async up() {
|
||||
await upsertPermissions();
|
||||
},
|
||||
});
|
||||
20
apps/meteor/server/startup/migrations/v298.ts
Normal file
20
apps/meteor/server/startup/migrations/v298.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Integrations } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 298,
|
||||
name: 'Set overrideDestinationChannelEnabled for all incoming webhook integrations',
|
||||
async up() {
|
||||
await Integrations.updateMany(
|
||||
{
|
||||
type: 'webhook-incoming',
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
overrideDestinationChannelEnabled: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v299.ts
Normal file
11
apps/meteor/server/startup/migrations/v299.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 299,
|
||||
name: 'Add public field to existing custom OAuth settings',
|
||||
async up() {
|
||||
await Settings.updateMany({ _id: /^Accounts_OAuth_Custom.+/, group: 'OAuth' }, { $set: { public: false } });
|
||||
},
|
||||
});
|
||||
27
apps/meteor/server/startup/migrations/v300.ts
Normal file
27
apps/meteor/server/startup/migrations/v300.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
import { settingsRegistry } from '../../settings';
|
||||
|
||||
addMigration({
|
||||
version: 300,
|
||||
async up() {
|
||||
const customOauthServices = await Settings.find({ _id: /Accounts_OAuth_Custom-[^-]+$/ }, { projection: { _id: 1 } }).toArray();
|
||||
const serviceNames = customOauthServices.map(({ _id }) => _id.replace('Accounts_OAuth_Custom-', ''));
|
||||
|
||||
for await (const serviceName of serviceNames) {
|
||||
await settingsRegistry.add(`Accounts_OAuth_Custom-${serviceName}-merge_users_distinct_services`, false, {
|
||||
type: 'boolean',
|
||||
group: 'OAuth',
|
||||
section: `Custom OAuth: ${serviceName}`,
|
||||
i18nLabel: 'Accounts_OAuth_Custom_Merge_Users_Distinct_Services',
|
||||
i18nDescription: 'Accounts_OAuth_Custom_Merge_Users_Distinct_Services_Description',
|
||||
enableQuery: {
|
||||
_id: `Accounts_OAuth_Custom-${serviceName}-merge_users`,
|
||||
value: true,
|
||||
},
|
||||
persistent: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
10
apps/meteor/server/startup/migrations/v301.ts
Normal file
10
apps/meteor/server/startup/migrations/v301.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 301,
|
||||
async up() {
|
||||
await Permissions.updateOne({ _id: 'call-management', roles: { $ne: 'user' } }, { $addToSet: { roles: 'user' } });
|
||||
},
|
||||
});
|
||||
16
apps/meteor/server/startup/migrations/v303.ts
Normal file
16
apps/meteor/server/startup/migrations/v303.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 303,
|
||||
name: 'Add roles field and set empty for users without it',
|
||||
async up() {
|
||||
await Users.updateMany(
|
||||
{
|
||||
roles: { $exists: false },
|
||||
},
|
||||
{ $set: { roles: [] } },
|
||||
);
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v304.ts
Normal file
22
apps/meteor/server/startup/migrations/v304.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Analytics } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 304,
|
||||
name: 'Drop wrong index from analytics collection',
|
||||
async up() {
|
||||
const indexes = await Analytics.col.indexes();
|
||||
|
||||
if (
|
||||
indexes.find(
|
||||
(index) => index.name === 'room._id_1_date_1' && index.partialFilterExpression && index.partialFilterExpression.type === 'rooms',
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Analytics.col.dropIndex('room._id_1_date_1');
|
||||
await Analytics.createIndexes();
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v305.ts
Normal file
11
apps/meteor/server/startup/migrations/v305.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 305,
|
||||
name: 'Remove unused view-history permission',
|
||||
async up() {
|
||||
await Permissions.deleteOne({ _id: 'view-history' });
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v306.ts
Normal file
22
apps/meteor/server/startup/migrations/v306.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 306,
|
||||
name: 'Adds missing ignoreThreads parameter for old rooms with retention policy overridden',
|
||||
async up() {
|
||||
await Rooms.updateMany(
|
||||
{
|
||||
'retention.enabled': true,
|
||||
'retention.overrideGlobal': true,
|
||||
'retention.ignoreThreads': { $exists: false },
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'retention.ignoreThreads': false,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
38
apps/meteor/server/startup/migrations/v307.ts
Normal file
38
apps/meteor/server/startup/migrations/v307.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Apps } from '@rocket.chat/apps';
|
||||
import type { IAppStorageItem } from '@rocket.chat/apps/dist/server/storage/IAppStorageItem';
|
||||
import { License } from '@rocket.chat/license';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 307,
|
||||
name: "Mark all installed private apps as 'migrated'",
|
||||
async up() {
|
||||
const isEE = License.hasValidLicense();
|
||||
if (isEE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Apps.self) {
|
||||
throw new Error('Apps Orchestrator not registered.');
|
||||
}
|
||||
|
||||
Apps.initialize();
|
||||
|
||||
const sigMan = Apps.getManager().getSignatureManager();
|
||||
const appsStorage = Apps.getStorage();
|
||||
const apps = await appsStorage.retrieveAllPrivate();
|
||||
|
||||
for (const app of apps.values()) {
|
||||
const updatedApp = {
|
||||
...app,
|
||||
migrated: true,
|
||||
} as IAppStorageItem;
|
||||
|
||||
await appsStorage.updatePartialAndReturnDocument({
|
||||
...updatedApp,
|
||||
signature: await sigMan.signApp(updatedApp),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
32
apps/meteor/server/startup/migrations/v308.ts
Normal file
32
apps/meteor/server/startup/migrations/v308.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ISetting } from '@rocket.chat/core-typings';
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
import { isValidCron } from 'cron-validator';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 308,
|
||||
name: 'Update packageValue from LDAP interval settings',
|
||||
async up() {
|
||||
const newAvatarSyncPackageValue = '0 0 * * *';
|
||||
const newAutoLogoutPackageValue = '*/5 * * * *';
|
||||
const ldapAvatarSyncInterval = await Settings.findOneById<Pick<ISetting, 'value'>>('LDAP_Background_Sync_Avatars_Interval', {
|
||||
projection: { value: 1 },
|
||||
});
|
||||
const ldapAutoLogoutInterval = await Settings.findOneById<Pick<ISetting, 'value'>>('LDAP_Sync_AutoLogout_Interval', {
|
||||
projection: { value: 1 },
|
||||
});
|
||||
const isValidAvatarSyncInterval = ldapAvatarSyncInterval && isValidCron(ldapAvatarSyncInterval.value as string);
|
||||
const isValidAutoLogoutInterval = ldapAutoLogoutInterval && isValidCron(ldapAutoLogoutInterval.value as string);
|
||||
|
||||
// TODO audit
|
||||
await Settings.updateOne(
|
||||
{ _id: 'LDAP_Background_Sync_Avatars_Interval' },
|
||||
{ $set: { packageValue: newAvatarSyncPackageValue, ...(!isValidAvatarSyncInterval && { value: newAvatarSyncPackageValue }) } },
|
||||
);
|
||||
await Settings.updateOne(
|
||||
{ _id: 'LDAP_Sync_AutoLogout_Interval' },
|
||||
{ $set: { packageValue: newAutoLogoutPackageValue, ...(!isValidAutoLogoutInterval && { value: newAutoLogoutPackageValue }) } },
|
||||
);
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v309.ts
Normal file
11
apps/meteor/server/startup/migrations/v309.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 309,
|
||||
name: 'Remove unused UI_Click_Direct_Message setting',
|
||||
async up() {
|
||||
await Settings.removeById('UI_Click_Direct_Message');
|
||||
},
|
||||
});
|
||||
29
apps/meteor/server/startup/migrations/v310.ts
Normal file
29
apps/meteor/server/startup/migrations/v310.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ISetting } from '@rocket.chat/core-typings';
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 310,
|
||||
name: 'Update translation key on "Forgot password" e-mail body setting',
|
||||
async up() {
|
||||
const forgotPasswordEmail = await Settings.findOneById<Pick<ISetting, 'value'>>('Forgot_Password_Email', { projection: { value: 1 } });
|
||||
if (!forgotPasswordEmail) {
|
||||
return;
|
||||
}
|
||||
const newPackageValue =
|
||||
'<h2>{Forgot_password}</h2><p>{Lets_get_you_new_one_}</p><a class="btn" href="[Forgot_Password_Url]">{Reset}</a><p class="advice">{If_you_didnt_ask_for_reset_ignore_this_email}</p>';
|
||||
|
||||
// TODO: audit
|
||||
|
||||
await Settings.updateOne(
|
||||
{ _id: 'Forgot_Password_Email' },
|
||||
{
|
||||
$set: {
|
||||
packageValue: newPackageValue,
|
||||
value: (forgotPasswordEmail.value as string).replace(/{Lets_get_you_new_one}/g, '{Lets_get_you_new_one_}'),
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v311.ts
Normal file
22
apps/meteor/server/startup/migrations/v311.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 311,
|
||||
name: 'Update default behavior of E2E_Enabled_Mentions setting, to allow mentions in encrypted messages by default.',
|
||||
async up() {
|
||||
// TODO: audit
|
||||
await Settings.updateOne(
|
||||
{
|
||||
_id: 'E2E_Enabled_Mentions',
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
value: true,
|
||||
packageValue: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
20
apps/meteor/server/startup/migrations/v312.ts
Normal file
20
apps/meteor/server/startup/migrations/v312.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { LivechatRooms, Rooms, Subscriptions, Users } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 312,
|
||||
async up() {
|
||||
try {
|
||||
await Promise.allSettled([
|
||||
LivechatRooms.col.dropIndex('v.token_1'),
|
||||
Rooms.col.dropIndex('t_1'),
|
||||
Subscriptions.col.dropIndex('rid_1'),
|
||||
Users.col.dropIndex('active_1'),
|
||||
]);
|
||||
} catch (error: unknown) {
|
||||
console.warn('Error dropping redundant indexes, continuing...');
|
||||
console.warn(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v313.ts
Normal file
11
apps/meteor/server/startup/migrations/v313.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 313,
|
||||
name: 'Remove "Call_Center" setting group',
|
||||
async up() {
|
||||
await Settings.deleteOne({ _id: 'Call_Center', type: 'group' });
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v314.ts
Normal file
22
apps/meteor/server/startup/migrations/v314.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 314,
|
||||
name: 'Update default behavior of E2E_Allow_Unencrypted_Messages setting, to not allow un-encrypted messages by default.',
|
||||
async up() {
|
||||
// TODO: audit
|
||||
await Settings.updateOne(
|
||||
{
|
||||
_id: 'E2E_Allow_Unencrypted_Messages',
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
value: false,
|
||||
packageValue: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
27
apps/meteor/server/startup/migrations/v315.ts
Normal file
27
apps/meteor/server/startup/migrations/v315.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { IPermission } from '@rocket.chat/core-typings';
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { upsertPermissions } from '../../lib/authorization/upsertPermissions';
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 315,
|
||||
name: 'Copy roles from add-team-channel permission to new create-team-channel, create-team-group and move-room-to-team permissions',
|
||||
async up() {
|
||||
// Calling upsertPermissions on purpose so that the new permissions are added before the migration runs
|
||||
await upsertPermissions();
|
||||
|
||||
const addTeamChannelPermission = await Permissions.findOneById<Pick<IPermission, 'roles'>>('add-team-channel', {
|
||||
projection: { roles: 1 },
|
||||
});
|
||||
|
||||
if (addTeamChannelPermission) {
|
||||
await Permissions.updateMany(
|
||||
{ _id: { $in: ['create-team-channel', 'create-team-group', 'move-room-to-team'] } },
|
||||
{ $set: { roles: addTeamChannelPermission.roles } },
|
||||
);
|
||||
|
||||
await Permissions.deleteOne({ _id: 'add-team-channel' });
|
||||
}
|
||||
},
|
||||
});
|
||||
20
apps/meteor/server/startup/migrations/v316.ts
Normal file
20
apps/meteor/server/startup/migrations/v316.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 316,
|
||||
name: 'Remove Cloud_Workspace_Access_Token and Cloud_Workspace_Access_Token_Expires_At from the settings collection',
|
||||
async up() {
|
||||
const accessToken = ((await Settings.getValueById('Cloud_Workspace_Access_Token')) as string) || '';
|
||||
const expirationDate = ((await Settings.getValueById('Cloud_Workspace_Access_Token_Expires_At')) as Date) || new Date(0);
|
||||
|
||||
if (accessToken) {
|
||||
await Settings.removeById('Cloud_Workspace_Access_Token');
|
||||
}
|
||||
|
||||
if (expirationDate) {
|
||||
await Settings.removeById('Cloud_Workspace_Access_Token_Expires_At');
|
||||
}
|
||||
},
|
||||
});
|
||||
124
apps/meteor/server/startup/migrations/v317.ts
Normal file
124
apps/meteor/server/startup/migrations/v317.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { ILoginServiceConfiguration, OAuthConfiguration } from '@rocket.chat/core-typings';
|
||||
import { Settings, LoginServiceConfiguration } from '@rocket.chat/models';
|
||||
import { isTruthy } from '@rocket.chat/tools';
|
||||
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
const newDefaultButtonColor = '#e4e7ea';
|
||||
const newDefaultButtonLabelColor = '#1f2329';
|
||||
|
||||
const settingsToUpdate = [
|
||||
// button background colors
|
||||
{ key: 'SAML_Custom_Default_button_color', defaultValue: '#1d74f5', newValue: newDefaultButtonColor },
|
||||
{ key: 'CAS_button_color', defaultValue: '#1d74f5', newValue: newDefaultButtonColor },
|
||||
{ key: 'Accounts_OAuth_Nextcloud_button_color', defaultValue: '#0082c9', newValue: newDefaultButtonColor },
|
||||
{ key: 'Accounts_OAuth_Dolphin_button_color', defaultValue: '#1d74f5', newValue: newDefaultButtonColor },
|
||||
// button label colors
|
||||
{ key: 'SAML_Custom_Default_button_label_color', defaultValue: '#1d74f5', newValue: newDefaultButtonLabelColor },
|
||||
{ key: 'CAS_button_label_color', defaultValue: '#1d74f5', newValue: newDefaultButtonLabelColor },
|
||||
{ key: 'Accounts_OAuth_Nextcloud_button_label_color', defaultValue: '#1d74f5', newValue: newDefaultButtonLabelColor },
|
||||
{ key: 'Accounts_OAuth_Dolphin_button_label_color', defaultValue: '#1d74f5', newValue: newDefaultButtonLabelColor },
|
||||
];
|
||||
|
||||
const getSettingValue = async (key: string) => Settings.getValueById(key);
|
||||
|
||||
async function updateOAuthServices(): Promise<void> {
|
||||
const services = await Settings.find({ _id: { $regex: /^(Accounts_OAuth_|Accounts_OAuth_Custom-)[a-z0-9_]+$/i } }).toArray();
|
||||
const filteredServices = services.filter(({ value }) => typeof value === 'boolean');
|
||||
for await (const { _id: key, value } of filteredServices) {
|
||||
if (value !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let serviceName = key.replace('Accounts_OAuth_', '');
|
||||
if (serviceName === 'Meteor') {
|
||||
serviceName = 'meteor-developer';
|
||||
}
|
||||
if (/Accounts_OAuth_Custom-/.test(key)) {
|
||||
serviceName = key.replace('Accounts_OAuth_Custom-', '');
|
||||
}
|
||||
|
||||
const serviceKey = serviceName.toLowerCase();
|
||||
|
||||
const data: Partial<ILoginServiceConfiguration & Omit<OAuthConfiguration, '_id'>> = {};
|
||||
|
||||
if (/Accounts_OAuth_Custom-/.test(key)) {
|
||||
data.buttonLabelColor = (await getSettingValue(`${key}-button_label_color`)) as string;
|
||||
data.buttonColor = (await getSettingValue(`${key}-button_color`)) as string;
|
||||
}
|
||||
|
||||
if (serviceName === 'Nextcloud') {
|
||||
data.buttonLabelColor = (await getSettingValue('Accounts_OAuth_Nextcloud_button_label_color')) as string;
|
||||
data.buttonColor = (await getSettingValue('Accounts_OAuth_Nextcloud_button_color')) as string;
|
||||
}
|
||||
|
||||
await LoginServiceConfiguration.createOrUpdateService(serviceKey, data);
|
||||
}
|
||||
}
|
||||
|
||||
addMigration({
|
||||
version: 317,
|
||||
name: 'Change default color of OAuth login services buttons',
|
||||
async up() {
|
||||
// TODO: audit
|
||||
const promises = settingsToUpdate
|
||||
.map(async ({ key, defaultValue, newValue }) => {
|
||||
const oldSettingValue = await getSettingValue(key);
|
||||
|
||||
if (!oldSettingValue || oldSettingValue !== defaultValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
SystemLogger.warn({ msg: 'The default value of the setting has changed. Please review your settings.', key, newValue });
|
||||
|
||||
return Settings.updateOne({ _id: key }, { $set: { value: newValue } });
|
||||
})
|
||||
.filter(isTruthy);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const customOAuthButtonColors = await Settings.find({
|
||||
_id: { $regex: /^Accounts_OAuth_Custom-[a-zA-Z0-9_-]+-button_color$/ },
|
||||
}).toArray();
|
||||
const customOAuthButtonLabelColors = await Settings.find({
|
||||
_id: { $regex: /^Accounts_OAuth_Custom-[a-zA-Z0-9_-]+-button_label_color$/ },
|
||||
}).toArray();
|
||||
|
||||
const buttonColorPromises = customOAuthButtonColors
|
||||
.map(({ _id, value, packageValue }) => {
|
||||
if (packageValue !== value) {
|
||||
return;
|
||||
}
|
||||
|
||||
SystemLogger.warn({
|
||||
msg: 'The default value of the custom setting has changed. Please review your settings.',
|
||||
_id,
|
||||
newValue: newDefaultButtonColor,
|
||||
});
|
||||
|
||||
return Settings.updateOne({ _id }, { $set: { value: newDefaultButtonColor } });
|
||||
})
|
||||
.filter(isTruthy);
|
||||
|
||||
const buttonLabelColorPromises = customOAuthButtonLabelColors
|
||||
.map(({ _id, value, packageValue }) => {
|
||||
if (packageValue !== value) {
|
||||
return;
|
||||
}
|
||||
|
||||
SystemLogger.warn({
|
||||
msg: 'The default value of the custom setting has changed. Please review your settings.',
|
||||
_id,
|
||||
newValue: newDefaultButtonLabelColor,
|
||||
});
|
||||
|
||||
return Settings.updateOne({ _id }, { $set: { value: newDefaultButtonLabelColor } });
|
||||
})
|
||||
.filter(isTruthy);
|
||||
|
||||
await Promise.all([...buttonColorPromises, ...buttonLabelColorPromises]);
|
||||
// update login service configurations
|
||||
await updateOAuthServices();
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v318.ts
Normal file
11
apps/meteor/server/startup/migrations/v318.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 318,
|
||||
name: 'Remove "E2E Encryption" setting group',
|
||||
async up() {
|
||||
await Settings.deleteOne({ _id: 'E2E Encryption', type: 'group' });
|
||||
},
|
||||
});
|
||||
15
apps/meteor/server/startup/migrations/v319.ts
Normal file
15
apps/meteor/server/startup/migrations/v319.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { LivechatRooms } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 319,
|
||||
name: 'Remove "pdfTranscriptRequested" index from LivechatRooms collection',
|
||||
async up() {
|
||||
try {
|
||||
await LivechatRooms.col.dropIndex('pdfTranscriptRequested_1');
|
||||
} catch {
|
||||
console.error('Error dropping index');
|
||||
}
|
||||
},
|
||||
});
|
||||
10
apps/meteor/server/startup/migrations/v320.ts
Normal file
10
apps/meteor/server/startup/migrations/v320.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 320,
|
||||
async up() {
|
||||
await Settings.deleteOne({ _id: 'API_Use_REST_For_DDP_Calls' });
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v321.ts
Normal file
11
apps/meteor/server/startup/migrations/v321.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 321,
|
||||
name: 'Remove voxtelesys & mobex related settings',
|
||||
async up() {
|
||||
await Promise.all([Settings.deleteMany({ section: { $in: ['Voxtelesys', 'Mobex'] } })]);
|
||||
},
|
||||
});
|
||||
21
apps/meteor/server/startup/migrations/v322.ts
Normal file
21
apps/meteor/server/startup/migrations/v322.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 322,
|
||||
name: 'Remove Tokenpass settings',
|
||||
async up() {
|
||||
await Settings.deleteMany({
|
||||
_id: {
|
||||
$in: [
|
||||
'Accounts_OAuth_Tokenpass',
|
||||
'API_Tokenpass_URL',
|
||||
'Accounts_OAuth_Tokenpass_id',
|
||||
'Accounts_OAuth_Tokenpass_secret',
|
||||
'Accounts_OAuth_Tokenpass_callback_url',
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
27
apps/meteor/server/startup/migrations/v323.ts
Normal file
27
apps/meteor/server/startup/migrations/v323.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { AppLogs } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
// Remove outdated indexes on rocketchat_apps_logs
|
||||
addMigration({
|
||||
version: 323,
|
||||
name: 'Remove outdated indexes on rocketchat_apps_logs',
|
||||
async up() {
|
||||
// Idempotent drops: ignore "IndexNotFound" (code 27)
|
||||
try {
|
||||
await AppLogs.col.dropIndex('appId_indexed_query');
|
||||
} catch (e: any) {
|
||||
if (e?.code !== 27 && e?.codeName !== 'IndexNotFound') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await AppLogs.col.dropIndex('general_logs_index');
|
||||
} catch (e: any) {
|
||||
if (e?.code !== 27 && e?.codeName !== 'IndexNotFound') {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v324.ts
Normal file
11
apps/meteor/server/startup/migrations/v324.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 324,
|
||||
name: 'Remove Log_View_Limit setting',
|
||||
async up() {
|
||||
await Settings.deleteOne({ _id: 'Log_View_Limit' });
|
||||
},
|
||||
});
|
||||
43
apps/meteor/server/startup/migrations/v325.ts
Normal file
43
apps/meteor/server/startup/migrations/v325.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Settings, Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 325,
|
||||
name: 'Remove VoIP_Omnichannel Settings and Permissions',
|
||||
async up() {
|
||||
await Settings.deleteMany({
|
||||
_id: {
|
||||
$in: [
|
||||
'VoIP_Omnichannel',
|
||||
'VoIP_Enabled',
|
||||
'VoIP_JWT_Secret',
|
||||
'Voip_Server_Configuration',
|
||||
'VoIP_Server_Name',
|
||||
'VoIP_Server_Websocket_Path',
|
||||
'VoIP_Retry_Count',
|
||||
'VoIP_Enable_Keep_Alive_For_Unstable_Networks',
|
||||
'VoIP_Management_Server_Host',
|
||||
'VoIP_Management_Server_Port',
|
||||
'VoIP_Management_Server_Name',
|
||||
'VoIP_Management_Server_Username',
|
||||
'VoIP_Management_Server_Password',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await Permissions.deleteMany({
|
||||
_id: {
|
||||
$in: [
|
||||
'manage-voip-call-settings',
|
||||
'manage-voip-contact-center-settings',
|
||||
'manage-agent-extension-association',
|
||||
'inbound-voip-calls',
|
||||
'spy-voip-calls',
|
||||
'outbound-voip-calls',
|
||||
'view-agent-extension-association',
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
23
apps/meteor/server/startup/migrations/v326.ts
Normal file
23
apps/meteor/server/startup/migrations/v326.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 326,
|
||||
name: 'Close Omnichannel VoIP Rooms',
|
||||
async up() {
|
||||
await Rooms.updateMany(
|
||||
{
|
||||
t: 'v' as any,
|
||||
open: {
|
||||
$exists: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
$unset: {
|
||||
open: 1,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v327.ts
Normal file
22
apps/meteor/server/startup/migrations/v327.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 327,
|
||||
name: 'Remove FreeSwitch Settings',
|
||||
async up() {
|
||||
await Settings.deleteMany({
|
||||
_id: {
|
||||
$in: [
|
||||
'VoIP_TeamCollab_FreeSwitch',
|
||||
'VoIP_TeamCollab_FreeSwitch_Host',
|
||||
'VoIP_TeamCollab_FreeSwitch_Port',
|
||||
'VoIP_TeamCollab_FreeSwitch_Password',
|
||||
'VoIP_TeamCollab_FreeSwitch_Timeout',
|
||||
'VoIP_TeamCollab_FreeSwitch_WebSocket_Path',
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
15
apps/meteor/server/startup/migrations/v328.ts
Normal file
15
apps/meteor/server/startup/migrations/v328.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 328,
|
||||
name: 'Remove Old Voip Permissions',
|
||||
async up() {
|
||||
await Permissions.deleteMany({
|
||||
_id: {
|
||||
$in: ['manage-voip-extensions', 'view-user-voip-extension', 'view-voip-extension-details'],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v329.ts
Normal file
11
apps/meteor/server/startup/migrations/v329.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 329,
|
||||
name: 'Remove VoIP Toggle Setting',
|
||||
async up() {
|
||||
await Settings.deleteOne({ _id: 'VoIP_TeamCollab_Enabled' });
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v330.ts
Normal file
11
apps/meteor/server/startup/migrations/v330.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 330,
|
||||
name: 'Remove ECDH setting',
|
||||
async up() {
|
||||
await Settings.deleteOne({ _id: 'ECDH_Enabled' });
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v331.ts
Normal file
11
apps/meteor/server/startup/migrations/v331.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Permissions } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 331,
|
||||
name: 'Remove view-livechat-current-chats permission',
|
||||
async up() {
|
||||
await Permissions.deleteOne({ _id: 'view-livechat-current-chats' });
|
||||
},
|
||||
});
|
||||
53
apps/meteor/server/startup/migrations/v332.ts
Normal file
53
apps/meteor/server/startup/migrations/v332.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { CallHistory, Users } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 332,
|
||||
name: 'Fill contact information on older call history entries',
|
||||
async up() {
|
||||
const cursor = CallHistory.col.aggregate([
|
||||
{
|
||||
$match: {
|
||||
external: false,
|
||||
contactId: { $exists: true },
|
||||
contactName: { $exists: false },
|
||||
contactUsername: { $exists: false },
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
$lookup: {
|
||||
from: Users.col.collectionName,
|
||||
localField: 'contactId',
|
||||
foreignField: '_id',
|
||||
as: 'contactDetails',
|
||||
},
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
contactName: { $first: '$contactDetails.name' },
|
||||
contactUsername: { $first: '$contactDetails.username' },
|
||||
},
|
||||
},
|
||||
{
|
||||
$project: {
|
||||
contactName: 1,
|
||||
contactUsername: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$merge: {
|
||||
into: CallHistory.col.collectionName,
|
||||
on: '_id',
|
||||
whenMatched: 'merge',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for await (const _item of cursor) {
|
||||
//
|
||||
}
|
||||
},
|
||||
});
|
||||
35
apps/meteor/server/startup/migrations/v333.ts
Normal file
35
apps/meteor/server/startup/migrations/v333.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 333,
|
||||
name: 'Remove Voxtelesys & Mobex SMS settings by ID',
|
||||
async up() {
|
||||
const ids = [
|
||||
// Voxtelesys settings
|
||||
'SMS_Voxtelesys_authToken',
|
||||
'SMS_Voxtelesys_URL',
|
||||
'SMS_Voxtelesys_FileUpload_Enabled',
|
||||
'SMS_Voxtelesys_FileUpload_MediaTypeWhiteList',
|
||||
|
||||
// Mobex settings
|
||||
'SMS_Mobex_gateway_address',
|
||||
'SMS_Mobex_gateway_address_desc',
|
||||
'SMS_Mobex_from_number',
|
||||
'SMS_Mobex_from_number_desc',
|
||||
'SMS_Mobex_from_numbers_list',
|
||||
'SMS_Mobex_from_numbers_list_desc',
|
||||
'SMS_Mobex_password',
|
||||
'SMS_Mobex_restful_address',
|
||||
'SMS_Mobex_restful_address_desc',
|
||||
'SMS_Mobex_username',
|
||||
];
|
||||
|
||||
await Settings.deleteMany({
|
||||
_id: { $in: ids },
|
||||
});
|
||||
|
||||
await Settings.findOneAndUpdate({ _id: 'SMS_Service', value: { $ne: 'twilio' } }, { $set: { value: 'twilio' } });
|
||||
},
|
||||
});
|
||||
22
apps/meteor/server/startup/migrations/v334.ts
Normal file
22
apps/meteor/server/startup/migrations/v334.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 334,
|
||||
name: 'Remove webRTC settings',
|
||||
async up() {
|
||||
await Settings.deleteMany({
|
||||
_id: {
|
||||
$in: [
|
||||
'WebRTC_Enabled',
|
||||
'WebRTC_Enable_Channel',
|
||||
'WebRTC_Enable_Private',
|
||||
'WebRTC_Enable_Direct',
|
||||
'WebRTC_Calls_Count',
|
||||
'WebRTC_Servers',
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
11
apps/meteor/server/startup/migrations/v335.ts
Normal file
11
apps/meteor/server/startup/migrations/v335.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
|
||||
import { addMigration } from '../../lib/migrations';
|
||||
|
||||
addMigration({
|
||||
version: 335,
|
||||
name: 'Remove OTR_Enable and OTR_Count settings',
|
||||
async up() {
|
||||
await Settings.deleteMany({ _id: { $in: ['OTR_Enable', 'OTR_Count'] } });
|
||||
},
|
||||
});
|
||||
102
apps/meteor/server/startup/migrations/xrun.ts
Normal file
102
apps/meteor/server/startup/migrations/xrun.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Permissions, Roles, Settings, Users } from '@rocket.chat/models';
|
||||
import type { UpdateResult } from 'mongodb';
|
||||
|
||||
import { upsertPermissions } from '../../lib/authorization/upsertPermissions';
|
||||
import { migrateDatabase, onServerVersionChange } from '../../lib/migrations';
|
||||
import { settings } from '../../settings';
|
||||
import { ensureCloudWorkspaceRegistered } from '../cloudRegistration';
|
||||
|
||||
const { MIGRATION_VERSION = 'latest' } = process.env;
|
||||
|
||||
const [version, ...subcommands] = MIGRATION_VERSION.split(',');
|
||||
|
||||
const maxAgeSettingMap = new Map([
|
||||
['RetentionPolicy_MaxAge_Channels', 'RetentionPolicy_TTL_Channels'],
|
||||
['RetentionPolicy_MaxAge_Groups', 'RetentionPolicy_TTL_Groups'],
|
||||
['RetentionPolicy_MaxAge_DMs', 'RetentionPolicy_TTL_DMs'],
|
||||
]);
|
||||
|
||||
const moveRetentionSetting = async () => {
|
||||
const convertDaysToMs = (days: number) => days * 24 * 60 * 60 * 1000;
|
||||
|
||||
const promises: Array<Promise<UpdateResult>> = [];
|
||||
await Settings.find(
|
||||
{ _id: { $in: Array.from(maxAgeSettingMap.keys()) }, value: { $ne: -1 } },
|
||||
{ projection: { _id: 1, value: 1 } },
|
||||
).forEach(({ _id, value }) => {
|
||||
const newSettingId = maxAgeSettingMap.get(_id);
|
||||
if (!newSettingId) {
|
||||
throw new Error(`moveRetentionSetting - Setting ${_id} equivalent does not exist`);
|
||||
}
|
||||
|
||||
const newValue = convertDaysToMs(Number(value));
|
||||
|
||||
promises.push(
|
||||
Settings.updateOne(
|
||||
{
|
||||
_id: maxAgeSettingMap.get(_id),
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
value: newValue,
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const currentCache = settings.getSetting(newSettingId);
|
||||
if (!currentCache) {
|
||||
return;
|
||||
}
|
||||
settings.set({ ...currentCache, value: newValue });
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
await Settings.updateMany({ _id: { $in: Array.from(maxAgeSettingMap.keys()) } }, { $set: { value: -1 } });
|
||||
};
|
||||
|
||||
async function setPermissionsToNewRole() {
|
||||
const role = await Roles.findOneById('federated-external');
|
||||
|
||||
// if role was found it means it was already created with the permissions, so we can skip it
|
||||
if (role) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if federation was used before the new role was created, we need to update their roles with the new role
|
||||
await Users.updateMany({ federated: true }, { $addToSet: { roles: 'federated-external' } });
|
||||
|
||||
const rolePermissions = [
|
||||
'create-c',
|
||||
'create-d',
|
||||
'create-p',
|
||||
'delete-own-message',
|
||||
'leave-c',
|
||||
'leave-p',
|
||||
'mention-all',
|
||||
'mention-here',
|
||||
'start-discussion',
|
||||
'start-discussion-other-user',
|
||||
'view-c-room',
|
||||
'view-d-room',
|
||||
'view-p-room',
|
||||
'preview-c-room',
|
||||
'view-outside-room',
|
||||
'mobile-upload-file',
|
||||
'access-federation',
|
||||
];
|
||||
|
||||
// since this is a one time query, no need to create a method in model class for it
|
||||
await Permissions.updateMany({ _id: { $in: rolePermissions } }, { $addToSet: { roles: 'federated-external' } });
|
||||
}
|
||||
|
||||
export const performMigrationProcedure = async (): Promise<void> => {
|
||||
await migrateDatabase(version === 'latest' ? version : parseInt(version), subcommands);
|
||||
// perform operations when the server is starting with a different version
|
||||
await onServerVersionChange(async () => {
|
||||
await setPermissionsToNewRole();
|
||||
await upsertPermissions();
|
||||
await ensureCloudWorkspaceRegistered();
|
||||
await moveRetentionSetting();
|
||||
});
|
||||
};
|
||||
12
apps/meteor/server/startup/presenceTroubleshoot.ts
Normal file
12
apps/meteor/server/startup/presenceTroubleshoot.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Presence } from '@rocket.chat/core-services';
|
||||
|
||||
import { settings } from '../settings';
|
||||
|
||||
// maybe this setting should disable the listener to 'presence.status' event on listerners.module.ts
|
||||
settings.watch('Troubleshoot_Disable_Presence_Broadcast', async (value) => {
|
||||
try {
|
||||
await Presence.toggleBroadcast(!value);
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
239
apps/meteor/server/startup/rateLimiter.js
Normal file
239
apps/meteor/server/startup/rateLimiter.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { RateLimiter } from 'meteor/rate-limit';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { sleep } from '../../lib/utils/sleep';
|
||||
import { metrics } from '../lib/metrics';
|
||||
import { settings } from '../settings';
|
||||
|
||||
const logger = new Logger('RateLimiter');
|
||||
|
||||
const slowDownRate = parseInt(process.env.RATE_LIMITER_SLOWDOWN_RATE);
|
||||
|
||||
const rateLimiterConsoleLog = ({ msg, reply, input }) => {
|
||||
console.warn('DDP RATE LIMIT:', msg);
|
||||
console.warn(JSON.stringify({ reply, input }, null, 2));
|
||||
};
|
||||
|
||||
const rateLimiterLogger = ({ msg, reply, input }) => logger.info({ msg, reply, input });
|
||||
|
||||
const rateLimiterLog = String(process.env.RATE_LIMITER_LOGGER) === 'console' ? rateLimiterConsoleLog : rateLimiterLogger;
|
||||
|
||||
// Get initial set of names already registered for rules
|
||||
const names = new Set(
|
||||
Object.values(DDPRateLimiter.printRules())
|
||||
.map((rule) => rule._matchers)
|
||||
.filter((match) => typeof match.name === 'string')
|
||||
.map((match) => match.name),
|
||||
);
|
||||
|
||||
// Override the addRule to save new names added after this point
|
||||
const { addRule } = DDPRateLimiter;
|
||||
DDPRateLimiter.addRule = (matcher, calls, time, callback) => {
|
||||
if (matcher && typeof matcher.name === 'string') {
|
||||
names.add(matcher.name);
|
||||
}
|
||||
return addRule.call(DDPRateLimiter, matcher, calls, time, callback);
|
||||
};
|
||||
|
||||
const { _increment } = DDPRateLimiter;
|
||||
DDPRateLimiter._increment = function (input) {
|
||||
const session = Meteor.server.sessions.get(input.connectionId);
|
||||
input.broadcastAuth = (session && session.connectionHandle && session.connectionHandle.broadcastAuth) === true;
|
||||
|
||||
return _increment.call(DDPRateLimiter, input);
|
||||
};
|
||||
|
||||
// Need to override the meteor's code duo to a problem with the callback reply
|
||||
// being shared among all matchs
|
||||
RateLimiter.prototype.check = function (input) {
|
||||
// ==== BEGIN OVERRIDE ====
|
||||
const session = Meteor.server.sessions.get(input.connectionId);
|
||||
input.broadcastAuth = (session && session.connectionHandle && session.connectionHandle.broadcastAuth) === true;
|
||||
// ==== END OVERRIDE ====
|
||||
|
||||
const self = this;
|
||||
const reply = {
|
||||
allowed: true,
|
||||
timeToReset: 0,
|
||||
numInvocationsLeft: Infinity,
|
||||
};
|
||||
|
||||
const matchedRules = self._findAllMatchingRules(input);
|
||||
_.each(matchedRules, (rule) => {
|
||||
// ==== BEGIN OVERRIDE ====
|
||||
const callbackReply = {
|
||||
allowed: true,
|
||||
timeToReset: 0,
|
||||
numInvocationsLeft: Infinity,
|
||||
};
|
||||
// ==== END OVERRIDE ====
|
||||
|
||||
const ruleResult = rule.apply(input);
|
||||
let numInvocations = rule.counters[ruleResult.key];
|
||||
|
||||
if (ruleResult.timeToNextReset < 0) {
|
||||
// Reset all the counters since the rule has reset
|
||||
rule.resetCounter();
|
||||
ruleResult.timeSinceLastReset = new Date().getTime() - rule._lastResetTime;
|
||||
ruleResult.timeToNextReset = rule.options.intervalTime;
|
||||
numInvocations = 0;
|
||||
}
|
||||
|
||||
if (numInvocations > rule.options.numRequestsAllowed) {
|
||||
// Only update timeToReset if the new time would be longer than the
|
||||
// previously set time. This is to ensure that if this input triggers
|
||||
// multiple rules, we return the longest period of time until they can
|
||||
// successfully make another call
|
||||
if (reply.timeToReset < ruleResult.timeToNextReset) {
|
||||
reply.timeToReset = ruleResult.timeToNextReset;
|
||||
}
|
||||
reply.allowed = false;
|
||||
reply.numInvocationsLeft = 0;
|
||||
|
||||
// ==== BEGIN OVERRIDE ====
|
||||
callbackReply.timeToReset = ruleResult.timeToNextReset;
|
||||
callbackReply.allowed = false;
|
||||
callbackReply.numInvocationsLeft = 0;
|
||||
callbackReply.numInvocationsExceeded = numInvocations - rule.options.numRequestsAllowed;
|
||||
rule._executeCallback(callbackReply, input);
|
||||
// ==== END OVERRIDE ====
|
||||
} else {
|
||||
// If this is an allowed attempt and we haven't failed on any of the
|
||||
// other rules that match, update the reply field.
|
||||
if (rule.options.numRequestsAllowed - numInvocations < reply.numInvocationsLeft && reply.allowed) {
|
||||
reply.timeToReset = ruleResult.timeToNextReset;
|
||||
reply.numInvocationsLeft = rule.options.numRequestsAllowed - numInvocations;
|
||||
}
|
||||
|
||||
// ==== BEGIN OVERRIDE ====
|
||||
callbackReply.timeToReset = ruleResult.timeToNextReset;
|
||||
callbackReply.numInvocationsLeft = rule.options.numRequestsAllowed - numInvocations;
|
||||
rule._executeCallback(callbackReply, input);
|
||||
// ==== END OVERRIDE ====
|
||||
}
|
||||
});
|
||||
return reply;
|
||||
};
|
||||
|
||||
const checkNameNonStream = (name) => name && !names.has(name) && !name.startsWith('stream-');
|
||||
const checkNameForStream = (name) => name && !names.has(name) && name.startsWith('stream-');
|
||||
|
||||
const ruleIds = {};
|
||||
|
||||
const callback = (msg, name) => async (reply, input) => {
|
||||
if (reply.allowed === false) {
|
||||
rateLimiterLog({ msg, reply, input });
|
||||
const rateLimitLabels = {
|
||||
limit_name: name,
|
||||
user_id: input.userId,
|
||||
client_address: input.clientAddress,
|
||||
type: input.type,
|
||||
name: input.name,
|
||||
connection_id: input.connectionId,
|
||||
};
|
||||
metrics.ddpRateLimitExceeded.inc(rateLimitLabels);
|
||||
metrics.ddpRateLimitExceededTotal.inc(rateLimitLabels);
|
||||
// sleep before sending the error to slow down next requests
|
||||
if (slowDownRate > 0 && reply.numInvocationsExceeded) {
|
||||
await sleep(slowDownRate * reply.numInvocationsExceeded);
|
||||
}
|
||||
// } else {
|
||||
// console.log('DDP RATE LIMIT:', message);
|
||||
// console.log(JSON.stringify({ ...reply, ...input }, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
const messages = {
|
||||
IP: 'address',
|
||||
User: 'userId',
|
||||
Connection: 'connectionId',
|
||||
User_By_Method: 'userId per method',
|
||||
Connection_By_Method: 'connectionId per method',
|
||||
};
|
||||
|
||||
const reconfigureLimit = Meteor.bindEnvironment((name, rules, factor = 1) => {
|
||||
if (ruleIds[name + factor]) {
|
||||
DDPRateLimiter.removeRule(ruleIds[name + factor]);
|
||||
}
|
||||
|
||||
if (!settings.get(`DDP_Rate_Limit_${name}_Enabled`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ruleIds[name + factor] = addRule(
|
||||
rules,
|
||||
settings.get(`DDP_Rate_Limit_${name}_Requests_Allowed`) * factor,
|
||||
settings.get(`DDP_Rate_Limit_${name}_Interval_Time`) * factor,
|
||||
callback(`limit by ${messages[name]}`, name),
|
||||
);
|
||||
});
|
||||
|
||||
const configIP = _.debounce(() => {
|
||||
reconfigureLimit('IP', {
|
||||
broadcastAuth: false,
|
||||
clientAddress: (clientAddress) => clientAddress !== '127.0.0.1',
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const configUser = _.debounce(() => {
|
||||
reconfigureLimit('User', {
|
||||
broadcastAuth: false,
|
||||
userId: (userId) => userId != null,
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const configConnection = _.debounce(() => {
|
||||
reconfigureLimit('Connection', {
|
||||
broadcastAuth: false,
|
||||
connectionId: () => true,
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
const configUserByMethod = _.debounce(() => {
|
||||
reconfigureLimit('User_By_Method', {
|
||||
broadcastAuth: false,
|
||||
type: () => true,
|
||||
name: checkNameNonStream,
|
||||
userId: (userId) => userId != null,
|
||||
});
|
||||
reconfigureLimit(
|
||||
'User_By_Method',
|
||||
{
|
||||
broadcastAuth: false,
|
||||
type: () => true,
|
||||
name: checkNameForStream,
|
||||
userId: (userId) => userId != null,
|
||||
},
|
||||
4,
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
const configConnectionByMethod = _.debounce(() => {
|
||||
reconfigureLimit('Connection_By_Method', {
|
||||
broadcastAuth: false,
|
||||
type: () => true,
|
||||
name: checkNameNonStream,
|
||||
connectionId: () => true,
|
||||
});
|
||||
reconfigureLimit(
|
||||
'Connection_By_Method',
|
||||
{
|
||||
broadcastAuth: false,
|
||||
type: () => true,
|
||||
name: checkNameForStream,
|
||||
connectionId: () => true,
|
||||
},
|
||||
4,
|
||||
);
|
||||
}, 1000);
|
||||
|
||||
if (!process.env.TEST_MODE) {
|
||||
settings.watchByRegex(/^DDP_Rate_Limit_IP_.+/, configIP);
|
||||
settings.watchByRegex(/^DDP_Rate_Limit_User_[^B].+/, configUser);
|
||||
settings.watchByRegex(/^DDP_Rate_Limit_Connection_[^B].+/, configConnection);
|
||||
settings.watchByRegex(/^DDP_Rate_Limit_User_By_Method_.+/, configUserByMethod);
|
||||
settings.watchByRegex(/^DDP_Rate_Limit_Connection_By_Method_.+/, configConnectionByMethod);
|
||||
}
|
||||
11
apps/meteor/server/startup/robots.js
Normal file
11
apps/meteor/server/startup/robots.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
|
||||
import { settings } from '../settings';
|
||||
|
||||
Meteor.startup(() => {
|
||||
return WebApp.connectHandlers.use('/robots.txt', (req, res /* , next*/) => {
|
||||
res.writeHead(200);
|
||||
res.end(settings.get('Robot_Instructions_File_Content'));
|
||||
});
|
||||
});
|
||||
131
apps/meteor/server/startup/serverRunning.ts
Normal file
131
apps/meteor/server/startup/serverRunning.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { License } from '@rocket.chat/license';
|
||||
// import { Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import semver from 'semver';
|
||||
|
||||
import { Info } from '../../app/utils/rocketchat.info';
|
||||
import { showErrorBox, showSuccessBox, showWarningBox } from '../lib/logger/showBox';
|
||||
import { getMongoInfo } from '../lib/utils/functions/getMongoInfo';
|
||||
// import { i18n } from '../lib/i18n';
|
||||
// import { isRunningMs } from '../lib/isRunningMs';
|
||||
// import { sendMessagesToAdmins } from '../lib/sendMessagesToAdmins';
|
||||
import { settings } from '../settings';
|
||||
|
||||
const exitIfNotBypassed = (ignore: string | undefined, errorCode = 1) => {
|
||||
if (typeof ignore === 'string' && ['yes', 'true'].includes(ignore.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.exit(errorCode);
|
||||
};
|
||||
|
||||
const skipMongoDbDeprecationCheck =
|
||||
['yes', 'true'].includes(String(process.env.SKIP_MONGODEPRECATION_CHECK).toLowerCase()) ||
|
||||
process.env.TEST_MODE === 'true' ||
|
||||
process.env.TEST_MODE === 'api' ||
|
||||
process.env.NODE_ENV === 'development';
|
||||
// const skipMongoDbDeprecationBanner = ['yes', 'true'].includes(String(process.env.SKIP_MONGODEPRECATION_BANNER).toLowerCase());
|
||||
|
||||
Meteor.startup(async () => {
|
||||
const { mongoVersion, mongoStorageEngine } = await getMongoInfo();
|
||||
|
||||
const desiredNodeVersion = semver.clean(fs.readFileSync(path.join(process.cwd(), '../../.node_version.txt')).toString());
|
||||
const parsedSemVer = semver.parse(desiredNodeVersion);
|
||||
if (!parsedSemVer) {
|
||||
console.error('Failed to parse desired Node.js version from .node_version.txt');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const desiredNodeVersionMajor = String(parsedSemVer.major);
|
||||
|
||||
return setTimeout(async () => {
|
||||
let msg: string | string[] = [
|
||||
`Rocket.Chat Version: ${Info.version}`,
|
||||
` NodeJS Version: ${process.versions.node} - ${process.arch}`,
|
||||
` MongoDB Version: ${mongoVersion}`,
|
||||
` MongoDB Engine: ${mongoStorageEngine}`,
|
||||
` Platform: ${process.platform}`,
|
||||
` Process Port: ${process.env.PORT}`,
|
||||
` Site URL: ${settings.get<string>('Site_Url')}`,
|
||||
` Hashed Site URL: ${License.getHashedWorkspaceUrl()}`,
|
||||
` OpenSSL Version: ${process.versions.openssl}`,
|
||||
` FIPS Provider: ${crypto.getFips() ? 'Enabled' : 'Disabled'}`,
|
||||
];
|
||||
|
||||
if (Info.commit?.hash) {
|
||||
msg.push(` Commit Hash: ${Info.commit.hash.substr(0, 10)}`);
|
||||
}
|
||||
|
||||
if (Info.commit?.branch) {
|
||||
msg.push(` Commit Branch: ${Info.commit.branch}`);
|
||||
}
|
||||
|
||||
msg = msg.join('\n');
|
||||
|
||||
if (!semver.satisfies(process.versions.node, desiredNodeVersionMajor)) {
|
||||
msg += [
|
||||
'',
|
||||
'',
|
||||
'YOUR CURRENT NODEJS VERSION IS NOT SUPPORTED,',
|
||||
`PLEASE UPGRADE / DOWNGRADE TO VERSION ${desiredNodeVersionMajor}.X.X`,
|
||||
].join('\n');
|
||||
showErrorBox('SERVER ERROR', msg);
|
||||
|
||||
exitIfNotBypassed(process.env.BYPASS_NODEJS_VALIDATION);
|
||||
}
|
||||
|
||||
const mongoSemver = semver.coerce(mongoVersion);
|
||||
|
||||
if (!mongoSemver || semver.satisfies(mongoSemver, '<7.0.0')) {
|
||||
msg += ['', '', 'YOUR CURRENT MONGODB VERSION IS NOT SUPPORTED BY ROCKET.CHAT,', 'PLEASE UPGRADE TO VERSION 8.0 OR LATER'].join('\n');
|
||||
showErrorBox('SERVER ERROR', msg);
|
||||
|
||||
exitIfNotBypassed(process.env.BYPASS_MONGO_VALIDATION);
|
||||
}
|
||||
|
||||
showSuccessBox('SERVER RUNNING', msg);
|
||||
|
||||
// Deprecation
|
||||
if (!skipMongoDbDeprecationCheck && semver.satisfies(semver.coerce(mongoVersion)!, '<8.0.0')) {
|
||||
msg = [
|
||||
`DEPRECATED: MONGODB VERSION ${mongoVersion}.`,
|
||||
'SUPPORT FOR MONGODB <8.0 WILL BE REMOVED IN ROCKET.CHAT 9.0.0',
|
||||
'PLEASE UPGRADE MONGODB TO VERSION 8.0 OR GREATER',
|
||||
].join('\n');
|
||||
showWarningBox('DEPRECATION', msg);
|
||||
|
||||
// const id = `mongodbDeprecation_${mongoVersion.replace(/[^0-9]/g, '_')}`;
|
||||
// const title = 'MongoDB_Deprecated';
|
||||
// const text = 'MongoDB_version_s_is_deprecated_please_upgrade_your_installation';
|
||||
// const link = 'https://go.rocket.chat/i/mongodb-deprecated';
|
||||
|
||||
// if (!(await Users.bannerExistsById(id))) {
|
||||
// if (skipMongoDbDeprecationBanner || process.env.TEST_MODE) {
|
||||
// return;
|
||||
// }
|
||||
// sendMessagesToAdmins({
|
||||
// msgs: async ({ adminUser }) => [
|
||||
// {
|
||||
// msg: `*${i18n.t(title, adminUser.language)}*\n${i18n.t(text, { postProcess: 'sprintf', sprintf: [mongoVersion] }, adminUser.language)}\n${link}`,
|
||||
// },
|
||||
// ],
|
||||
// banners: [
|
||||
// {
|
||||
// id,
|
||||
// priority: 100,
|
||||
// title,
|
||||
// text,
|
||||
// textArguments: [mongoVersion],
|
||||
// modifiers: ['danger'],
|
||||
// link,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// }
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
Reference in New Issue
Block a user