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

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
type UserFields = {
[k: string]: number;
};
export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({
'name': 1,
'username': 1,
'nickname': 1,
'emails': 1,
'status': 1,
'statusDefault': 1,
'statusText': 1,
'statusSource': 1,
'statusExpiresAt': 1,
'statusConnection': 1,
'bio': 1,
'avatarOrigin': 1,
'utcOffset': 1,
'language': 1,
'settings': 1,
'enableAutoAway': 1,
'idleTimeLimit': 1,
'roles': 1,
'active': 1,
'defaultRoom': 1,
'customFields': 1,
'requirePasswordChange': 1,
'requirePasswordChangeReason': 1,
'statusLivechat': 1,
'banners': 1,
'oauth.authorizedClients': 1,
'_updatedAt': 1,
'avatarETag': 1,
'openBusinessHours': 1,
'abacAttributes': 1,
...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }),
});

View File

@@ -0,0 +1,12 @@
import { getBaseUserFields } from './getBaseUserFields';
type UserFields = {
[k: string]: number;
};
export const getDefaultUserFields = (): UserFields => ({
...getBaseUserFields(true),
'services.github': 1,
'services.gitlab': 1,
'services.password.bcrypt': 1,
});

View File

@@ -0,0 +1,61 @@
import { MongoInternals, type MongoConnection } from 'meteor/mongo';
function getOplogInfo(): { mongo: MongoConnection } {
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
return { mongo };
}
async function fallbackMongoInfo(): Promise<{
mongoVersion: string;
mongoStorageEngine?: string;
mongo: MongoConnection;
}> {
let mongoVersion;
let mongoStorageEngine;
const { mongo } = getOplogInfo();
try {
const { version } = await mongo.db.command({ buildinfo: 1 });
mongoVersion = version;
mongoStorageEngine = 'unknown';
} catch (e) {
console.error('=== Error getting MongoDB info ===');
console.error(e?.toString());
console.error('----------------------------------');
console.error("Without mongodb version we can't ensure you are running a compatible version.");
console.error('If you are running your mongodb with auth enabled and an user different from admin');
console.error('you may need to grant permissions for this user to check cluster data.');
console.error('You can do it via mongo shell running the following command replacing');
console.error("the string YOUR_USER by the correct user's name:");
console.error('');
console.error(' db.runCommand({ grantRolesToUser: "YOUR_USER" , roles: [{role: "clusterMonitor", db: "admin"}]})');
console.error('');
console.error('==================================');
}
return { mongoVersion, mongoStorageEngine, mongo };
}
export async function getMongoInfo(): Promise<{
mongoVersion: string;
mongoStorageEngine?: string;
mongo: MongoConnection;
}> {
let mongoVersion;
let mongoStorageEngine;
const { mongo } = getOplogInfo();
try {
const { version, storageEngine } = await mongo.db.command({ serverStatus: 1 });
mongoVersion = version;
mongoStorageEngine = storageEngine.name;
} catch (e) {
return fallbackMongoInfo();
}
return { mongoVersion, mongoStorageEngine, mongo };
}

View File

@@ -0,0 +1,31 @@
import fs from 'node:fs';
function hasDockerEnv(): boolean {
try {
fs.statSync('/.dockerenv');
return true;
} catch (err) {
return false;
}
}
function hasDockerCGroup(): boolean {
try {
return fs.readFileSync('/proc/self/cgroup', 'utf8').indexOf('docker') !== -1;
} catch (err) {
return false;
}
}
function check(): boolean {
return hasDockerEnv() || hasDockerCGroup();
}
let _isDocker: boolean;
export const isDocker = function (): boolean {
if (_isDocker === undefined) {
_isDocker = check();
}
return _isDocker;
};

View File

@@ -0,0 +1,6 @@
import { settings } from '../../../settings';
export const isSMTPConfigured = (): boolean => {
const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined);
return Boolean(settings.get('SMTP_Host')) || isMailURLSet;
};

View File

@@ -0,0 +1,30 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { Uploads } from '@rocket.chat/models';
import { FileUpload } from '../../media/file-upload';
import { getURL } from '../getURL';
export const normalizeMessageFileUpload = async (message: Omit<IMessage, '_updatedAt'>): Promise<Omit<IMessage, '_updatedAt'>> => {
if (message.file && !message.fileUpload) {
const jwt = FileUpload.generateJWTToFileUrls({
rid: message.rid,
userId: message.u._id,
fileId: message.file._id,
});
const file = await Uploads.findOne({ _id: message.file._id });
if (!file) {
return message;
}
message.fileUpload = {
publicFilePath: file.name
? getURL(`${FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`).substring(1)}${jwt ? `?token=${jwt}` : ''}`, {
cdn: false,
full: true,
})
: '',
type: file.type,
size: file.size,
};
}
return message;
};

View File

@@ -0,0 +1,10 @@
import { getURL } from './getURL';
export const getAvatarURL = ({ username, roomId, cache }: { username?: string; roomId?: string; cache?: string }): string | undefined => {
if (username) {
return getURL(`/avatar/${encodeURIComponent(username)}${cache ? `?etag=${cache}` : ''}`);
}
if (roomId) {
return getURL(`/avatar/room/${encodeURIComponent(roomId)}${cache ? `?etag=${cache}` : ''}`);
}
};

View File

@@ -0,0 +1,19 @@
import { getURLWithoutSettings } from '../../../app/utils/lib/getURL';
import { settings } from '../../settings';
export const getURL = function (
path: string,
params: {
cdn?: boolean;
full?: boolean;
cloud?: boolean;
cloud_route?: string;
cloud_params?: Record<string, string>;
} = {},
cloudDeepLinkUrl?: string,
): string {
const cdnPrefix = settings.get<string>('CDN_PREFIX') || '';
const siteUrl = settings.get<string>('Site_Url') || '';
return getURLWithoutSettings(path, params, cdnPrefix, siteUrl, cloudDeepLinkUrl);
};

View File

@@ -0,0 +1,9 @@
import { getAvatarURL } from './getAvatarURL';
export const getUserAvatarURL = function (username: string, cache = ''): string | undefined {
if (username == null) {
return;
}
return getAvatarURL({ username, cache });
};

View File

@@ -0,0 +1,51 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { settings } from '../../settings';
export const getUserNotificationPreference = async (user: IUser | string, pref: string) => {
if (typeof user === 'string') {
const u = await Users.findOneById(user);
if (!u) {
return null;
}
user = u;
}
let preferenceKey: 'desktopNotifications' | 'pushNotifications' | 'emailNotificationMode' | undefined;
switch (pref) {
case 'desktop':
preferenceKey = 'desktopNotifications';
break;
case 'mobile':
preferenceKey = 'pushNotifications';
break;
case 'email':
preferenceKey = 'emailNotificationMode';
break;
}
if (!preferenceKey) {
return null;
}
if (
user?.settings?.preferences &&
typeof user.settings.preferences[preferenceKey] !== 'undefined' &&
user.settings.preferences[preferenceKey] !== 'default'
) {
return {
value: user.settings.preferences[preferenceKey],
origin: 'user',
};
}
const serverValue = settings.get(`Accounts_Default_User_Preferences_${preferenceKey}`);
if (typeof serverValue !== 'undefined') {
return {
value: serverValue,
origin: 'server',
};
}
return null;
};

View File

@@ -0,0 +1,299 @@
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const jsrsasignStub = {
KJUR: {
jws: {
IntDate: {
get: sinon.stub(),
},
JWS: {
sign: sinon.stub(),
verify: sinon.stub(),
parse: sinon.stub(),
},
},
},
};
const { generateJWT, validateAndDecodeJWT } = proxyquire.noCallThru().load('./JWTHelper', {
jsrsasign: jsrsasignStub,
});
describe('JWTHelper', () => {
beforeEach(() => {
jsrsasignStub.KJUR.jws.IntDate.get.reset();
jsrsasignStub.KJUR.jws.JWS.sign.reset();
jsrsasignStub.KJUR.jws.JWS.verify.reset();
jsrsasignStub.KJUR.jws.JWS.parse.reset();
});
describe('generateJWT', () => {
it('should generate a JWT with the correct payload structure', () => {
const now = 1609459200;
const expiryDuration = 3600; // 1 hour in seconds
const expiryTime = now + expiryDuration;
jsrsasignStub.KJUR.jws.IntDate.get.callsFake((timeStr) => {
if (timeStr === 'now') return now;
if (timeStr === 'now + 1hour') return expiryTime;
return now;
});
jsrsasignStub.KJUR.jws.JWS.sign.returns('mocked.jwt.token');
const payload = { userId: 'user-123', roomId: 'room-456' };
const secret = 'test-secret';
const result = generateJWT(payload, secret);
expect(result).to.equal('mocked.jwt.token');
expect(jsrsasignStub.KJUR.jws.JWS.sign.calledOnce).to.be.true;
const signArgs = jsrsasignStub.KJUR.jws.JWS.sign.firstCall.args;
expect(signArgs[0]).to.equal('HS256');
expect(signArgs[1]).to.equal(JSON.stringify({ typ: 'JWT', alg: 'HS256' }));
const tokenPayload = JSON.parse(signArgs[2]);
expect(tokenPayload.iat).to.equal(now);
expect(tokenPayload.nbf).to.equal(now);
expect(tokenPayload.exp).to.equal(expiryTime);
expect(tokenPayload.aud).to.equal('RocketChat');
expect(tokenPayload.context).to.deep.equal(payload);
expect(signArgs[3]).to.deep.equal({ rstr: secret });
});
it('should include the provided payload in the context field', () => {
jsrsasignStub.KJUR.jws.IntDate.get.returns(1609459200);
jsrsasignStub.KJUR.jws.JWS.sign.returns('mocked.jwt.token');
const payload = { fileId: 'file-123', rid: 'room-123', userId: 'user-123' };
const secret = 'secret-key';
generateJWT(payload, secret);
const signArgs = jsrsasignStub.KJUR.jws.JWS.sign.firstCall.args;
const tokenPayload = JSON.parse(signArgs[2]);
expect(tokenPayload.context).to.deep.equal(payload);
});
it('should generate a JWT with custom audience when provided in options', () => {
jsrsasignStub.KJUR.jws.IntDate.get.returns(1609459200);
jsrsasignStub.KJUR.jws.JWS.sign.returns('mocked.jwt.token');
const payload = { userId: 'user-123' };
const secret = 'test-secret';
const customAudience = 'CustomApp';
generateJWT(payload, secret, { aud: customAudience });
const signArgs = jsrsasignStub.KJUR.jws.JWS.sign.firstCall.args;
const tokenPayload = JSON.parse(signArgs[2]);
expect(tokenPayload.aud).to.equal(customAudience);
});
});
describe('validateAndDecodeJWT', () => {
it('should return null if jwt is empty', () => {
const result = validateAndDecodeJWT('', 'secret');
expect(result).to.be.null;
});
it('should return null if jwt is null', () => {
const result = validateAndDecodeJWT(null as any, 'secret');
expect(result).to.be.null;
});
it('should return null if secret is empty', () => {
const result = validateAndDecodeJWT('some.jwt.token', '');
expect(result).to.be.null;
});
it('should return null if secret is null', () => {
const result = validateAndDecodeJWT('some.jwt.token', null as any);
expect(result).to.be.null;
});
it('should return null if signature is invalid', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(false);
const result = validateAndDecodeJWT('invalid.jwt.token', 'secret');
expect(result).to.be.null;
expect(jsrsasignStub.KJUR.jws.JWS.verify.calledOnce).to.be.true;
expect(jsrsasignStub.KJUR.jws.JWS.verify.calledWith('invalid.jwt.token', 'secret', ['HS256'])).to.be.true;
});
it('should return null if token is expired', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
nbf: Math.floor(Date.now() / 1000) - 7200,
aud: 'RocketChat',
context: { userId: 'user-123' },
},
});
const result = validateAndDecodeJWT('expired.jwt.token', 'secret');
expect(result).to.be.null;
});
it('should return null if nbf (not before) is in the future', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
exp: Math.floor(Date.now() / 1000) + 7200,
nbf: Math.floor(Date.now() / 1000) + 3600, // Not valid until 1 hour from now
aud: 'RocketChat',
context: { userId: 'user-123' },
},
});
const result = validateAndDecodeJWT('future.jwt.token', 'secret');
expect(result).to.be.null;
});
it('should return null if audience is not RocketChat', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
exp: Math.floor(Date.now() / 1000) + 3600,
nbf: Math.floor(Date.now() / 1000) - 60,
aud: 'WrongAudience',
context: { userId: 'user-123' },
},
});
const result = validateAndDecodeJWT('wrong-audience.jwt.token', 'secret');
expect(result).to.be.null;
});
it('should return the context payload for a valid token with correct signature, timestamps, and audience', () => {
const contextPayload = { userId: 'user-123', fileId: 'file-456', rid: 'room-789' };
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
exp: Math.floor(Date.now() / 1000) + 3600, // Expires in 1 hour
nbf: Math.floor(Date.now() / 1000) - 60, // Valid since 1 minute ago
aud: 'RocketChat',
context: contextPayload,
},
});
const result = validateAndDecodeJWT('valid.jwt.token', 'secret');
expect(result).to.deep.equal(contextPayload);
expect(jsrsasignStub.KJUR.jws.JWS.verify.calledOnce).to.be.true;
expect(jsrsasignStub.KJUR.jws.JWS.parse.calledOnce).to.be.true;
});
it('should return the context payload if exp and nbf are not provided', () => {
const contextPayload = { userId: 'user-123' };
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
aud: 'RocketChat',
context: contextPayload,
},
});
const result = validateAndDecodeJWT('valid.jwt.token', 'secret');
expect(result).to.deep.equal(contextPayload);
});
it('should return null if token cannot be parsed', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.throws(new Error('Parse error'));
const result = validateAndDecodeJWT('malformed.jwt.token', 'secret');
expect(result).to.be.null;
});
it('should return null if verification throws an error', () => {
jsrsasignStub.KJUR.jws.JWS.verify.throws(new Error('Verification error'));
const result = validateAndDecodeJWT('error.jwt.token', 'secret');
expect(result).to.be.null;
});
it('should validate and decode token with custom audience when provided in options', () => {
const contextPayload = { userId: 'user-123' };
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
exp: Math.floor(Date.now() / 1000) + 3600,
nbf: Math.floor(Date.now() / 1000) - 60,
aud: 'CustomApp',
context: contextPayload,
},
});
const result = validateAndDecodeJWT('valid.jwt.token', 'secret', { aud: 'CustomApp' });
expect(result).to.deep.equal(contextPayload);
});
it('should return null if custom audience does not match token audience', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
exp: Math.floor(Date.now() / 1000) + 3600,
nbf: Math.floor(Date.now() / 1000) - 60,
aud: 'CustomApp',
context: { userId: 'user-123' },
},
});
const result = validateAndDecodeJWT('valid.jwt.token', 'secret', { aud: 'DifferentApp' });
expect(result).to.be.null;
});
it('should return null if payloadObj is null', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: null,
});
const result = validateAndDecodeJWT('jwt.token', 'secret');
expect(result).to.be.null;
});
it('should return null if context is missing from payload', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns({
payloadObj: {
iat: 1609459200,
nbf: 1609459200,
exp: 1609462800,
aud: 'RocketChat',
},
});
const result = validateAndDecodeJWT('jwt.token', 'secret');
expect(result).to.be.null;
});
it('should return null if parse returns undefined', () => {
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
jsrsasignStub.KJUR.jws.JWS.parse.returns(undefined);
const result = validateAndDecodeJWT('jwt.token', 'secret');
expect(result).to.be.null;
});
});
});

View File

@@ -0,0 +1,56 @@
import jsr from 'jsrsasign';
const HEADER = {
typ: 'JWT',
alg: 'HS256',
};
export const generateJWT = (payload: Record<string, any>, secret: string, options?: { aud?: string }): string => {
const tokenPayload = {
iat: jsr.KJUR.jws.IntDate.get('now'),
nbf: jsr.KJUR.jws.IntDate.get('now'),
exp: jsr.KJUR.jws.IntDate.get('now + 1hour'),
aud: options?.aud || 'RocketChat',
context: payload,
};
const header = JSON.stringify(HEADER);
return jsr.KJUR.jws.JWS.sign(HEADER.alg, header, JSON.stringify(tokenPayload), { rstr: secret });
};
export const validateAndDecodeJWT = (jwt: string, secret: string, options?: { aud?: string }): Record<string, any> | null => {
if (!jwt || !secret) {
return null;
}
try {
const isSignatureValid = jsr.KJUR.jws.JWS.verify(jwt, secret, [HEADER.alg]);
if (!isSignatureValid) {
return null;
}
const decoded = jsr.KJUR.jws.JWS.parse(jwt);
if (!decoded?.payloadObj) {
return null;
}
const payload = decoded.payloadObj as Record<string, any>;
const now = Math.floor(Date.now() / 1000);
if (typeof payload.exp === 'number' && now >= payload.exp) {
return null;
}
if (typeof payload.nbf === 'number' && now < payload.nbf) {
return null;
}
if (payload.aud !== (options?.aud || 'RocketChat')) {
return null;
}
return payload.context || null;
} catch (error) {
return null;
}
};

View File

@@ -0,0 +1,3 @@
const colors = ['#158D65', '#7F1B9F', '#B68D00', '#E26D0E', '#10529E', '#6C727A'];
export const getAvatarColor = (name: string): string => colors[name.length % colors.length];

View File

@@ -0,0 +1,35 @@
import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings';
/**
* @type {(userPref: Pick<import('@rocket.chat/core-typings').IUser, 'settings'>) => {
* desktopPrefOrigin: 'user';
* mobilePrefOrigin: 'user';
* emailPrefOrigin: 'user';
* }}
*/
export const getDefaultSubscriptionPref = (userPref: AtLeast<IUser, 'settings'>) => {
const subscription: Partial<ISubscription> = {};
const { desktopNotifications, pushNotifications, emailNotificationMode, highlights } = userPref.settings?.preferences || {};
if (Array.isArray(highlights) && highlights.length) {
subscription.userHighlights = highlights;
}
if (desktopNotifications && desktopNotifications !== 'default') {
subscription.desktopNotifications = desktopNotifications;
subscription.desktopPrefOrigin = 'user';
}
if (pushNotifications && pushNotifications !== 'default') {
subscription.mobilePushNotifications = pushNotifications;
subscription.mobilePrefOrigin = 'user';
}
if (emailNotificationMode && emailNotificationMode !== 'default') {
subscription.emailNotifications = emailNotificationMode;
subscription.emailPrefOrigin = 'user';
}
return subscription;
};

View File

@@ -0,0 +1,32 @@
import moment from 'moment-timezone';
import { settings } from '../../../settings';
const padOffset = (offset: string | number): string => {
const numberOffset = Number(offset);
const absOffset = Math.abs(numberOffset);
const isNegative = !(numberOffset === absOffset);
return `${isNegative ? '-' : '+'}${absOffset < 10 ? `0${absOffset}` : absOffset}:00`;
};
const guessTimezoneFromOffset = (offset?: string | number): string => {
if (!offset) {
return moment.tz.guess();
}
return moment.tz.names().find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess();
};
export const getTimezone = (user?: { utcOffset?: string | number } | null): string => {
const timezone = settings.get('Default_Timezone_For_Reporting');
switch (timezone) {
case 'custom':
return settings.get<string>('Default_Custom_Timezone');
case 'user':
return guessTimezoneFromOffset(user?.utcOffset);
default:
return moment.tz.guess();
}
};

View File

@@ -0,0 +1,32 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { settings } from '../../../settings';
/**
* @summary Get a user preference
* @param {String} userId The user ID
* @param {String} preference The preference name
* @param {unknown?} defaultValue The default value
* @returns {unknown} The preference value
*/
export const getUserPreference = async (user: IUser | string, key: string, defaultValue: any = undefined) => {
let preference;
if (typeof user === 'string') {
const dbUser = await Users.findOneById(user, { projection: { [`settings.preferences.${key}`]: 1 } });
if (dbUser) {
user = dbUser;
}
}
if (typeof user === 'string') {
return defaultValue;
}
if (user?.settings?.preferences?.hasOwnProperty(key)) {
preference = user.settings.preferences[key];
} else if (defaultValue === undefined) {
preference = settings.get(`Accounts_Default_User_Preferences_${key}`);
}
return preference !== undefined ? preference : defaultValue;
};

View File

@@ -0,0 +1,73 @@
import { Rooms } from '@rocket.chat/models';
import { escapeHTML } from '@rocket.chat/tools';
import limax from 'limax';
import { Meteor } from 'meteor/meteor';
import { settings } from '../../../settings';
import { validateName } from '../../shared/validateName';
export const getValidRoomName = async (displayName: string, rid = '', options: { allowDuplicates?: boolean } = {}) => {
let slugifiedName = displayName;
if (settings.get('UI_Allow_room_names_with_special_chars')) {
const cleanName = limax(displayName, { maintainCase: true });
if (options.allowDuplicates !== true) {
const room = await Rooms.findOneByDisplayName(displayName);
if (room && room._id !== rid) {
if (room.archived) {
throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${cleanName}`, {
function: 'RocketChat.getValidRoomName',
channel_name: cleanName,
});
} else {
throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${cleanName}' exists`, {
function: 'RocketChat.getValidRoomName',
channel_name: cleanName,
});
}
}
}
slugifiedName = cleanName;
}
let nameValidation;
try {
nameValidation = new RegExp(`^${settings.get('UTF8_Channel_Names_Validation')}$`);
} catch (error) {
nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
}
if (!nameValidation.test(slugifiedName) || !validateName(slugifiedName)) {
throw new Meteor.Error('error-invalid-room-name', `${escapeHTML(slugifiedName)} is not a valid room name.`, {
function: 'RocketChat.getValidRoomName',
channel_name: escapeHTML(slugifiedName),
});
}
if (options.allowDuplicates !== true) {
const room = await Rooms.findOneByName(slugifiedName);
if (room && room._id !== rid) {
if (settings.get('UI_Allow_room_names_with_special_chars')) {
let tmpName = slugifiedName;
let next = 0;
while (await Rooms.findOneByNameAndNotId(tmpName, rid)) {
tmpName = `${slugifiedName}-${++next}`;
}
slugifiedName = tmpName;
} else if (room.archived) {
throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${escapeHTML(slugifiedName)}`, {
function: 'RocketChat.getValidRoomName',
channel_name: escapeHTML(slugifiedName),
});
} else {
throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${escapeHTML(slugifiedName)}' exists`, {
function: 'RocketChat.getValidRoomName',
channel_name: escapeHTML(slugifiedName),
});
}
}
}
return slugifiedName;
};

View File

@@ -0,0 +1,88 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { settings } from '../../../settings';
type NormalizableMessage = Pick<IMessage, 'u' | 'starred' | 'mentions' | 'reactions'>;
const filterStarred = <T extends Pick<IMessage, 'starred'>>(message: T, uid?: string): T => {
// if Allow_anonymous_read is enabled, uid will be undefined
if (!uid) return message;
// only return starred field if user has it starred
if (message.starred && Array.isArray(message.starred)) {
message.starred = message.starred.filter((star) => star._id === uid);
}
return message;
};
// TODO: we should let clients get user names on demand instead of doing this
function getNameOfUsername(users: Map<string, string>, username: string): string {
return users.get(username) || username;
}
export const normalizeMessagesForUser = async <T extends NormalizableMessage = IMessage>(messages: T[], uid?: string): Promise<T[]> => {
// if not using real names, there is nothing else to do
if (!settings.get('UI_Use_Real_Name')) {
return messages.map((message) => filterStarred(message, uid));
}
const usernames: Set<string> = new Set();
messages.forEach((message) => {
message = filterStarred(message, uid);
if (!message.u?.username) {
return;
}
usernames.add(message.u.username);
(message.mentions || []).forEach(({ username }) => {
if (username) {
usernames.add(username);
}
});
Object.values(message.reactions || {}).forEach((reaction) => reaction.usernames.forEach((username) => usernames.add(username)));
});
const names = new Map();
(
await Users.findUsersByUsernames([...usernames.values()], {
projection: {
username: 1,
name: 1,
},
}).toArray()
).forEach((user) => {
names.set(user.username, user.name);
});
messages.forEach((message) => {
if (!message.u) {
return;
}
message.u.name = getNameOfUsername(names, message.u.username);
(message.mentions || []).forEach((mention) => {
if (mention.username) {
mention.name = getNameOfUsername(names, mention.username);
}
});
if (!message.reactions) {
return messages;
}
message.reactions = Object.fromEntries(
Object.entries(message.reactions).map(([keys, reaction]) => {
reaction.names = reaction.usernames.map((username) => getNameOfUsername(names, username));
return [keys, reaction];
}),
);
});
return messages;
};

View File

@@ -0,0 +1,33 @@
import { Logger } from '@rocket.chat/logger';
const logger = new Logger('TemplateVarHandler');
export const templateVarHandler = function (variable: string, object: Record<string, any>): string | undefined {
const templateRegex = /#{([\w\-]+)}/gi;
let match = templateRegex.exec(variable);
let tmpVariable = variable;
if (match == null) {
if (!object.hasOwnProperty(variable)) {
logger?.debug({ msg: 'User does not have attribute', attribute: variable });
return;
}
return object[variable];
}
logger?.debug('template found. replacing values');
while (match != null) {
const tmplVar = match[0];
const tmplAttrName = match[1];
if (!object.hasOwnProperty(tmplAttrName)) {
logger?.debug({ msg: 'User does not have attribute', attribute: tmplAttrName });
return;
}
const attrVal = object[tmplAttrName];
logger?.debug({ msg: 'Replacing template var', templateVar: tmplVar, value: attrVal });
tmpVariable = tmpVariable.replace(tmplVar, attrVal);
match = templateRegex.exec(variable);
}
return tmpVariable;
};

View File

@@ -0,0 +1,35 @@
import { strLeft, strRightBack } from '../../../lib/utils/stringUtils';
import { settings } from '../../settings';
export const placeholders = {
replace: (
str: string,
data: { name?: string; email?: string; password?: string; reason?: string; user?: string; room?: string; unsubscribe?: string },
) => {
if (!str) {
return '';
}
str = str.replace(/\[Site_Name\]/g, settings.get<string>('Site_Name') || '');
str = str.replace(/\[Site_URL\]/g, settings.get<string>('Site_Url') || '');
if (data) {
str = str.replace(/\[name\]/g, data.name || '');
str = str.replace(/\[fname\]/g, strLeft(data.name, ' ') || '');
str = str.replace(/\[lname\]/g, strRightBack(data.name, ' ') || '');
str = str.replace(/\[email\]/g, data.email || '');
str = str.replace(/\[password\]/g, data.password || '');
str = str.replace(/\[reason\]/g, data.reason || '');
str = str.replace(/\[User\]/g, data.user || '');
str = str.replace(/\[Room\]/g, data.room || '');
if (data.unsubscribe) {
str = str.replace(/\[unsubscribe\]/g, data.unsubscribe);
}
}
str = str.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2');
return str;
},
};

View File

@@ -0,0 +1,9 @@
import { fileUploadIsValidContentTypeFromSettings } from '../../../app/utils/lib/restrictions';
import { settings } from '../../settings';
export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean {
const blackList = settings.get<string>('FileUpload_MediaTypeBlackList');
const whiteList = customWhiteList || settings.get<string>('FileUpload_MediaTypeWhiteList');
return fileUploadIsValidContentTypeFromSettings(type, whiteList, blackList);
};

View File

@@ -0,0 +1,166 @@
import { MeteorError } from '@rocket.chat/core-services';
import type {
IMessage,
SlashCommand,
SlashCommandOptions,
RequiredField,
SlashCommandPreviewItem,
SlashCommandPreviews,
} from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Meteor } from 'meteor/meteor';
import { methodDeprecationLogger } from '../deprecationWarningLogger';
interface ISlashCommandAddParams<T extends string> {
command: string;
callback?: SlashCommand<T>['callback'];
options?: SlashCommandOptions;
result?: SlashCommand['result'];
providesPreview?: boolean;
previewer?: SlashCommand['previewer'];
previewCallback?: SlashCommand['previewCallback'];
appId?: string;
description?: string;
}
export const slashCommands = {
commands: {} as Record<string, SlashCommand>,
add<T extends string>({
command,
callback,
options = {},
result,
providesPreview = false,
previewer,
previewCallback,
appId,
description = '',
}: ISlashCommandAddParams<T>): void {
if (this.commands[command]) {
return;
}
this.commands[command] = {
command,
callback,
params: options.params,
description: options.description || description,
permission: options.permission,
clientOnly: options.clientOnly || false,
result,
providesPreview: Boolean(providesPreview),
previewer,
previewCallback,
appId,
} as SlashCommand;
},
async run({
command,
message,
params,
triggerId,
userId,
}: {
command: string;
params: string;
message: RequiredField<Partial<IMessage>, 'rid' | '_id'>;
userId: string;
triggerId?: string | undefined;
}): Promise<unknown> {
const cmd = this.commands[command];
if (typeof cmd?.callback !== 'function') {
return;
}
if (!message?.rid) {
throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
}
return cmd.callback({ command, params, message, triggerId, userId });
},
async getPreviews(
command: string,
params: string,
message: RequiredField<Partial<IMessage>, 'rid'>,
userId: string,
): Promise<SlashCommandPreviews | undefined> {
const cmd = this.commands[command];
if (typeof cmd?.previewer !== 'function') {
return;
}
if (!message?.rid) {
throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
}
const previewInfo = await cmd.previewer(command, params, message, userId);
if (!previewInfo?.items?.length) {
return;
}
// A limit of ten results, to save time and bandwidth
if (previewInfo.items.length >= 10) {
previewInfo.items = previewInfo.items.slice(0, 10);
}
return previewInfo;
},
async executePreview(
command: string,
params: string,
message: Pick<IMessage, 'rid'> & Partial<Omit<IMessage, 'rid'>>,
preview: SlashCommandPreviewItem,
userId: string,
triggerId?: string,
) {
const cmd = this.commands[command];
if (typeof cmd?.previewCallback !== 'function') {
return;
}
if (!message?.rid) {
throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
}
// { id, type, value }
if (!preview.id || !preview.type || !preview.value) {
throw new MeteorError('error-invalid-preview', 'Preview Item must have an id, type, and value.');
}
return cmd.previewCallback(command, params, message, preview, userId, triggerId);
},
};
declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
slashCommand(params: { cmd: string; params: string; msg: IMessage; triggerId: string }): unknown;
}
}
Meteor.methods<ServerMethods>({
async slashCommand(command) {
methodDeprecationLogger.method('slashCommand', '9.0.0', '/v1/commands.run');
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'slashCommand',
});
}
if (!command?.cmd || !slashCommands.commands[command.cmd]) {
throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', {
method: 'executeSlashCommandPreview',
});
}
return slashCommands.run({
command: command.cmd,
params: command.params,
message: command.msg,
triggerId: command.triggerId,
userId,
});
},
});