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:
70
apps/meteor/lib/ClientLogger.ts
Normal file
70
apps/meteor/lib/ClientLogger.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* This class implements logger.
|
||||
* @remarks
|
||||
*/
|
||||
enum LogLevel {
|
||||
fatal,
|
||||
error,
|
||||
warn,
|
||||
info,
|
||||
debug,
|
||||
verbose,
|
||||
}
|
||||
|
||||
export class ClientLogger {
|
||||
module: string | undefined;
|
||||
|
||||
logLevel: LogLevel | undefined;
|
||||
|
||||
constructor(module: string, _level: LogLevel = LogLevel.info) {
|
||||
this.logLevel = _level;
|
||||
this.module = module;
|
||||
}
|
||||
|
||||
private writeLog(level: LogLevel, log: any): void {
|
||||
const logLine = `${new Date().toISOString()} ${LogLevel[level]} ${this.module}::${log}`;
|
||||
if (this.logLevel && this.logLevel < level) {
|
||||
return;
|
||||
}
|
||||
switch (level) {
|
||||
case LogLevel.warn:
|
||||
console.warn(logLine);
|
||||
break;
|
||||
case LogLevel.error:
|
||||
case LogLevel.fatal:
|
||||
console.error(logLine);
|
||||
break;
|
||||
default: {
|
||||
console.log(logLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verbose(...args: any[]): void {
|
||||
this.writeLog(LogLevel.verbose, args);
|
||||
}
|
||||
|
||||
debug(...args: any[]): void {
|
||||
this.writeLog(LogLevel.debug, args);
|
||||
}
|
||||
|
||||
info(...args: any[]): void {
|
||||
this.writeLog(LogLevel.info, args);
|
||||
}
|
||||
|
||||
warn(...args: any[]): void {
|
||||
this.writeLog(LogLevel.warn, args);
|
||||
}
|
||||
|
||||
error(...args: any[]): void {
|
||||
this.writeLog(LogLevel.error, args);
|
||||
}
|
||||
|
||||
fatal(...args: any[]): void {
|
||||
this.writeLog(LogLevel.fatal, args);
|
||||
}
|
||||
|
||||
setLogLevel(level: LogLevel): void {
|
||||
this.logLevel = level;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { IAppStorageItem } from '@rocket.chat/apps/dist/server/storage/IAppStorageItem';
|
||||
import type { LicenseAppSources } from '@rocket.chat/core-typings';
|
||||
|
||||
/**
|
||||
* There have been reports of apps not being correctly migrated from versions prior to 6.0
|
||||
*
|
||||
* This function is a workaround to get the installation source of an app from the app storage item
|
||||
* even if the installationSource property is not set.
|
||||
*/
|
||||
export function getInstallationSourceFromAppStorageItem(item: IAppStorageItem): LicenseAppSources {
|
||||
return item.installationSource || ('marketplaceInfo' in item ? 'marketplace' : 'private');
|
||||
}
|
||||
5
apps/meteor/lib/constants.ts
Normal file
5
apps/meteor/lib/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const NOTIFICATION_ATTACHMENT_COLOR = '#FD745E';
|
||||
export const MAX_MULTIPLE_UPLOADED_FILES = 10;
|
||||
export const MAX_CUSTOM_SOUND_SIZE_BYTES = 5242880;
|
||||
export const CUSTOM_SOUND_ALLOWED_MIME_TYPES = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/x-wav'];
|
||||
export const MESSAGE_MAX_PARSE_LENGTH_DEFAULT = 0;
|
||||
20
apps/meteor/lib/createQuoteAttachment.ts
Normal file
20
apps/meteor/lib/createQuoteAttachment.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { isTranslatedMessage, getUserDisplayName } from '@rocket.chat/core-typings';
|
||||
import type { ITranslatedMessage, IMessage } from '@rocket.chat/core-typings';
|
||||
|
||||
export function createQuoteAttachment(
|
||||
message: IMessage | ITranslatedMessage,
|
||||
messageLink: string,
|
||||
useRealName: boolean,
|
||||
userAvatarUrl: string,
|
||||
) {
|
||||
return {
|
||||
text: message.msg,
|
||||
...(message.md && { md: message.md }),
|
||||
...(isTranslatedMessage(message) && { translations: message?.translations }),
|
||||
message_link: messageLink,
|
||||
author_name: message.alias || getUserDisplayName(message.u.name, message.u.username, useRealName),
|
||||
author_icon: userAvatarUrl,
|
||||
attachments: message.attachments || [],
|
||||
ts: message.ts,
|
||||
};
|
||||
}
|
||||
5
apps/meteor/lib/errors/CloudOfflineLicenseError.ts
Normal file
5
apps/meteor/lib/errors/CloudOfflineLicenseError.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CloudWorkspaceError } from './CloudWorkspaceError';
|
||||
|
||||
export class CloudOfflineLicenseError extends CloudWorkspaceError {
|
||||
override name = CloudOfflineLicenseError.name;
|
||||
}
|
||||
5
apps/meteor/lib/errors/CloudWorkspaceAccessError.ts
Normal file
5
apps/meteor/lib/errors/CloudWorkspaceAccessError.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CloudWorkspaceError } from './CloudWorkspaceError';
|
||||
|
||||
export class CloudWorkspaceAccessError extends CloudWorkspaceError {
|
||||
override name = CloudWorkspaceAccessError.name;
|
||||
}
|
||||
5
apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts
Normal file
5
apps/meteor/lib/errors/CloudWorkspaceConnectionError.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CloudWorkspaceError } from './CloudWorkspaceError';
|
||||
|
||||
export class CloudWorkspaceConnectionError extends CloudWorkspaceError {
|
||||
override name = CloudWorkspaceConnectionError.name;
|
||||
}
|
||||
3
apps/meteor/lib/errors/CloudWorkspaceError.ts
Normal file
3
apps/meteor/lib/errors/CloudWorkspaceError.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class CloudWorkspaceError extends Error {
|
||||
override name = CloudWorkspaceError.name;
|
||||
}
|
||||
5
apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts
Normal file
5
apps/meteor/lib/errors/CloudWorkspaceLicenseError.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { CloudWorkspaceError } from './CloudWorkspaceError';
|
||||
|
||||
export class CloudWorkspaceLicenseError extends CloudWorkspaceError {
|
||||
override name = CloudWorkspaceLicenseError.name;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { CloudWorkspaceError } from './CloudWorkspaceError';
|
||||
|
||||
export class CloudWorkspaceRegistrationError extends CloudWorkspaceError {
|
||||
override name = CloudWorkspaceRegistrationError.name;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export class InvalidCloudAnnouncementInteractionError extends Error {
|
||||
override name = InvalidCloudAnnouncementInteractionError.name;
|
||||
}
|
||||
3
apps/meteor/lib/errors/InvalidCoreAppInteractionError.ts
Normal file
3
apps/meteor/lib/errors/InvalidCoreAppInteractionError.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export class InvalidCoreAppInteractionError extends Error {
|
||||
override name = InvalidCoreAppInteractionError.name;
|
||||
}
|
||||
12
apps/meteor/lib/getCronAdvancedTimerFromPrecisionSetting.ts
Normal file
12
apps/meteor/lib/getCronAdvancedTimerFromPrecisionSetting.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function getCronAdvancedTimerFromPrecisionSetting(precision: '0' | '1' | '2' | '3'): string {
|
||||
switch (precision) {
|
||||
case '0':
|
||||
return '*/30 * * * *'; // 30 minutes
|
||||
case '1':
|
||||
return '0 * * * *'; // hour
|
||||
case '2':
|
||||
return '0 */6 * * *'; // 6 hours
|
||||
case '3':
|
||||
return '0 0 * * *'; // day
|
||||
}
|
||||
}
|
||||
99
apps/meteor/lib/getDateFnsLocale.ts
Normal file
99
apps/meteor/lib/getDateFnsLocale.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import type { Locale } from 'date-fns';
|
||||
import { enUS } from 'date-fns/locale';
|
||||
|
||||
type LocaleLoader = () => Promise<Locale>;
|
||||
|
||||
// Maps Rocket.Chat language codes (from packages/i18n) to date-fns locale loaders.
|
||||
// Codes without a direct date-fns counterpart fall back to the closest match, or
|
||||
// to enUS when no reasonable counterpart exists.
|
||||
const localeLoaders: Record<string, LocaleLoader> = {
|
||||
'af': async () => (await import('date-fns/locale/af')).af,
|
||||
'ar': async () => (await import('date-fns/locale/ar')).ar,
|
||||
'az': async () => (await import('date-fns/locale/az')).az,
|
||||
'be-BY': async () => (await import('date-fns/locale/be')).be,
|
||||
'bg': async () => (await import('date-fns/locale/bg')).bg,
|
||||
'bn-BD': async () => (await import('date-fns/locale/bn')).bn,
|
||||
'bn-IN': async () => (await import('date-fns/locale/bn')).bn,
|
||||
'bs': async () => (await import('date-fns/locale/bs')).bs,
|
||||
'ca': async () => (await import('date-fns/locale/ca')).ca,
|
||||
'cs': async () => (await import('date-fns/locale/cs')).cs,
|
||||
'cy': async () => (await import('date-fns/locale/cy')).cy,
|
||||
'da': async () => (await import('date-fns/locale/da')).da,
|
||||
'de': async () => (await import('date-fns/locale/de')).de,
|
||||
'de-AT': async () => (await import('date-fns/locale/de-AT')).deAT,
|
||||
'de-IN': async () => (await import('date-fns/locale/de')).de,
|
||||
'el': async () => (await import('date-fns/locale/el')).el,
|
||||
'en': async () => (await import('date-fns/locale/en-US')).enUS,
|
||||
'eo': async () => (await import('date-fns/locale/eo')).eo,
|
||||
'es': async () => (await import('date-fns/locale/es')).es,
|
||||
'et': async () => (await import('date-fns/locale/et')).et,
|
||||
'eu': async () => (await import('date-fns/locale/eu')).eu,
|
||||
'fa': async () => (await import('date-fns/locale/fa-IR')).faIR,
|
||||
'fi': async () => (await import('date-fns/locale/fi')).fi,
|
||||
'fr': async () => (await import('date-fns/locale/fr')).fr,
|
||||
'gl': async () => (await import('date-fns/locale/gl')).gl,
|
||||
'he': async () => (await import('date-fns/locale/he')).he,
|
||||
'hi': async () => (await import('date-fns/locale/hi')).hi,
|
||||
'hi-IN': async () => (await import('date-fns/locale/hi')).hi,
|
||||
'hr': async () => (await import('date-fns/locale/hr')).hr,
|
||||
'hu': async () => (await import('date-fns/locale/hu')).hu,
|
||||
'id': async () => (await import('date-fns/locale/id')).id,
|
||||
'it': async () => (await import('date-fns/locale/it')).it,
|
||||
'ja': async () => (await import('date-fns/locale/ja')).ja,
|
||||
'ka-GE': async () => (await import('date-fns/locale/ka')).ka,
|
||||
'km': async () => (await import('date-fns/locale/km')).km,
|
||||
'ko': async () => (await import('date-fns/locale/ko')).ko,
|
||||
'ku': async () => (await import('date-fns/locale/ckb')).ckb,
|
||||
'lt': async () => (await import('date-fns/locale/lt')).lt,
|
||||
'lv': async () => (await import('date-fns/locale/lv')).lv,
|
||||
'mn': async () => (await import('date-fns/locale/mn')).mn,
|
||||
'ms-MY': async () => (await import('date-fns/locale/ms')).ms,
|
||||
'nb': async () => (await import('date-fns/locale/nb')).nb,
|
||||
'nl': async () => (await import('date-fns/locale/nl')).nl,
|
||||
'nn': async () => (await import('date-fns/locale/nn')).nn,
|
||||
'pl': async () => (await import('date-fns/locale/pl')).pl,
|
||||
'pt': async () => (await import('date-fns/locale/pt')).pt,
|
||||
'pt-BR': async () => (await import('date-fns/locale/pt-BR')).ptBR,
|
||||
'ro': async () => (await import('date-fns/locale/ro')).ro,
|
||||
'ru': async () => (await import('date-fns/locale/ru')).ru,
|
||||
'se': async () => (await import('date-fns/locale/se')).se,
|
||||
'sk': async () => (await import('date-fns/locale/sk')).sk,
|
||||
'sk-SK': async () => (await import('date-fns/locale/sk')).sk,
|
||||
'sl-SI': async () => (await import('date-fns/locale/sl')).sl,
|
||||
'sq': async () => (await import('date-fns/locale/sq')).sq,
|
||||
'sr': async () => (await import('date-fns/locale/sr')).sr,
|
||||
'sv': async () => (await import('date-fns/locale/sv')).sv,
|
||||
'ta-IN': async () => (await import('date-fns/locale/ta')).ta,
|
||||
'th-TH': async () => (await import('date-fns/locale/th')).th,
|
||||
'tr': async () => (await import('date-fns/locale/tr')).tr,
|
||||
'ug': async () => (await import('date-fns/locale/ug')).ug,
|
||||
'uk': async () => (await import('date-fns/locale/uk')).uk,
|
||||
'vi-VN': async () => (await import('date-fns/locale/vi')).vi,
|
||||
'zh': async () => (await import('date-fns/locale/zh-CN')).zhCN,
|
||||
'zh-HK': async () => (await import('date-fns/locale/zh-HK')).zhHK,
|
||||
'zh-TW': async () => (await import('date-fns/locale/zh-TW')).zhTW,
|
||||
};
|
||||
|
||||
const resolveLoader = (language: string): LocaleLoader | undefined => {
|
||||
if (localeLoaders[language]) {
|
||||
return localeLoaders[language];
|
||||
}
|
||||
const base = language.split('-').shift();
|
||||
if (base && localeLoaders[base]) {
|
||||
return localeLoaders[base];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getDateFnsLocale = async (language: string): Promise<Locale> => {
|
||||
const loader = resolveLoader(language);
|
||||
if (!loader) {
|
||||
return enUS;
|
||||
}
|
||||
try {
|
||||
return await loader();
|
||||
} catch (error) {
|
||||
console.error('Error loading date-fns locale:', error);
|
||||
return enUS;
|
||||
}
|
||||
};
|
||||
14
apps/meteor/lib/getImageExtensionFromMime.ts
Normal file
14
apps/meteor/lib/getImageExtensionFromMime.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const extensionsMap: Record<string, string> = {
|
||||
'image/apng': 'apng',
|
||||
'image/avif': 'avif',
|
||||
'image/gif': 'gif',
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/svg+xml': 'svg',
|
||||
'image/webp': 'webp',
|
||||
'image/bmp': 'bmp',
|
||||
'image/x-icon': 'ico',
|
||||
'image/tiff': 'tif',
|
||||
};
|
||||
|
||||
export const getImageExtensionFromMime = (mime: string): string | undefined => extensionsMap[mime];
|
||||
6
apps/meteor/lib/getMessageMaxParseLength.ts
Normal file
6
apps/meteor/lib/getMessageMaxParseLength.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { MESSAGE_MAX_PARSE_LENGTH_DEFAULT } from './constants';
|
||||
|
||||
export function getMessageMaxParseLength(): number {
|
||||
const parsed = Number.parseInt(process.env.MESSAGE_MAX_PARSE_LENGTH ?? '', 10);
|
||||
return Number.isFinite(parsed) ? parsed : MESSAGE_MAX_PARSE_LENGTH_DEFAULT;
|
||||
}
|
||||
2
apps/meteor/lib/getMessageUrlRegex.ts
Normal file
2
apps/meteor/lib/getMessageUrlRegex.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const getMessageUrlRegex = (): RegExp =>
|
||||
/([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g;
|
||||
64
apps/meteor/lib/getSubscriptionUnreadData.ts
Normal file
64
apps/meteor/lib/getSubscriptionUnreadData.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
const getUnreadTitle = (
|
||||
{
|
||||
mentions,
|
||||
threads,
|
||||
groupMentions,
|
||||
total,
|
||||
}: {
|
||||
mentions: number;
|
||||
threads: number;
|
||||
groupMentions: number;
|
||||
total: number;
|
||||
},
|
||||
t: TFunction,
|
||||
) => {
|
||||
const title = [] as string[];
|
||||
if (mentions) {
|
||||
title.push(t('mentions_counter', { count: mentions }));
|
||||
}
|
||||
if (threads) {
|
||||
title.push(t('threads_counter', { count: threads }));
|
||||
}
|
||||
if (groupMentions) {
|
||||
title.push(t('group_mentions_counter', { count: groupMentions }));
|
||||
}
|
||||
const count = total - mentions - groupMentions - threads;
|
||||
if (count > 0) {
|
||||
title.push(t('unread_messages_counter', { count }));
|
||||
}
|
||||
|
||||
return title.join(', ');
|
||||
};
|
||||
|
||||
export type UnreadData = Pick<
|
||||
SubscriptionWithRoom,
|
||||
'alert' | 'userMentions' | 'unread' | 'tunread' | 'tunreadUser' | 'groupMentions' | 'hideMentionStatus' | 'hideUnreadStatus'
|
||||
>;
|
||||
|
||||
export const getSubscriptionUnreadData = (
|
||||
{ userMentions, tunreadUser, tunread, unread, groupMentions, hideMentionStatus, hideUnreadStatus, alert }: UnreadData,
|
||||
t: TFunction,
|
||||
) => {
|
||||
const unreadCount = {
|
||||
mentions: userMentions + (tunreadUser?.length || 0),
|
||||
threads: tunread?.length || 0,
|
||||
groupMentions,
|
||||
total: unread + (tunread?.length || 0),
|
||||
};
|
||||
|
||||
const unreadTitle = getUnreadTitle(unreadCount, t);
|
||||
|
||||
const unreadVariant: 'primary' | 'warning' | 'danger' | 'secondary' =
|
||||
(unreadCount.mentions && 'danger') || (unreadCount.threads && 'primary') || (unreadCount.groupMentions && 'warning') || 'secondary';
|
||||
|
||||
const showUnread =
|
||||
(!hideUnreadStatus || (!hideMentionStatus && (Boolean(unreadCount.mentions) || Boolean(unreadCount.groupMentions)))) &&
|
||||
Boolean(unreadCount.total);
|
||||
|
||||
const highlightUnread = Boolean(!hideUnreadStatus && (alert || unread));
|
||||
|
||||
return { unreadTitle, unreadVariant, showUnread, unreadCount, highlightUnread };
|
||||
};
|
||||
20
apps/meteor/lib/getUserDisplayNames.ts
Normal file
20
apps/meteor/lib/getUserDisplayNames.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
import { normalizeUsername } from './utils/normalizeUsername';
|
||||
|
||||
/*
|
||||
In contrary to getUserDisplayName, this function returns an array of strings, containing name & username in the order they're supposed to be displayed.
|
||||
*/
|
||||
export const getUserDisplayNames = (
|
||||
name: IUser['name'],
|
||||
username: IUser['username'],
|
||||
useRealName: boolean,
|
||||
): [nameOrUsername: string, username?: string] => {
|
||||
if (!username) {
|
||||
throw new Error('Username is required');
|
||||
}
|
||||
const shouldUseName = !!(useRealName && name);
|
||||
const normalizedUsername = normalizeUsername(username);
|
||||
|
||||
return shouldUseName ? [name, normalizedUsername] : [normalizedUsername];
|
||||
};
|
||||
4
apps/meteor/lib/getUserEmailAddress.ts
Normal file
4
apps/meteor/lib/getUserEmailAddress.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
export const getUserEmailAddress = (user: Pick<IUser, 'emails'>): string | undefined =>
|
||||
Array.isArray(user.emails) ? user.emails.find(({ address }) => !!address)?.address : undefined;
|
||||
132
apps/meteor/lib/mapLivechatContactConflicts.spec.ts
Normal file
132
apps/meteor/lib/mapLivechatContactConflicts.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import type { Serialized, ILivechatContact } from '@rocket.chat/core-typings';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { mapLivechatContactConflicts } from './mapLivechatContactConflicts';
|
||||
|
||||
const sampleContact: Serialized<ILivechatContact> = {
|
||||
_id: 'any',
|
||||
name: 'Contact Name',
|
||||
createdAt: '',
|
||||
_updatedAt: '',
|
||||
channels: [],
|
||||
};
|
||||
|
||||
describe('Map Livechat Contact Conflicts', () => {
|
||||
it('should return an empty object when the contact has no conflicts', () => {
|
||||
expect(mapLivechatContactConflicts({ ...sampleContact })).to.be.equal({});
|
||||
});
|
||||
|
||||
it('should group conflicts of the same field in a single atribute', () => {
|
||||
expect(
|
||||
mapLivechatContactConflicts({
|
||||
...sampleContact,
|
||||
name: '',
|
||||
conflictingFields: [
|
||||
{
|
||||
field: 'name',
|
||||
value: 'First Name',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
value: 'Second Name',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).to.be.deep.equal({
|
||||
name: {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
values: ['First Name', 'Second Name'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should include the current value from the contact on the list of values of the conflict', () => {
|
||||
expect(
|
||||
mapLivechatContactConflicts({
|
||||
...sampleContact,
|
||||
name: 'Current Name',
|
||||
conflictingFields: [
|
||||
{
|
||||
field: 'name',
|
||||
value: 'First Name',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
value: 'Second Name',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).to.be.deep.equal({
|
||||
name: {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
values: ['First Name', 'Second Name', 'Current Name'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a separate attribute for each conflicting field', () => {
|
||||
expect(
|
||||
mapLivechatContactConflicts({
|
||||
...sampleContact,
|
||||
conflictingFields: [
|
||||
{
|
||||
field: 'name',
|
||||
value: 'First Value',
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
value: 'Second Value',
|
||||
},
|
||||
{
|
||||
field: 'manager',
|
||||
value: '1',
|
||||
},
|
||||
{
|
||||
field: 'manager',
|
||||
value: '2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
).to.be.deep.equal({
|
||||
name: {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
values: ['First Name', 'Second Name', 'Contact Name'],
|
||||
},
|
||||
contactManager: {
|
||||
name: 'contactManager',
|
||||
label: 'Manager',
|
||||
values: ['1', '2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should map conflicts on custom fields too', () => {
|
||||
expect(
|
||||
mapLivechatContactConflicts(
|
||||
{
|
||||
...sampleContact,
|
||||
conflictingFields: [
|
||||
{
|
||||
field: 'customFields.fullName',
|
||||
value: 'Full Name 1',
|
||||
},
|
||||
{
|
||||
field: 'customFields.fullName',
|
||||
value: 'Full Name 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
[{ name: 'fullName', type: 'text', label: 'Full Name' }],
|
||||
),
|
||||
).to.be.deep.equal({
|
||||
fullName: {
|
||||
name: 'fullName',
|
||||
label: 'Full Name',
|
||||
values: ['Full Name 1', 'Full Name 2'],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
46
apps/meteor/lib/mapLivechatContactConflicts.ts
Normal file
46
apps/meteor/lib/mapLivechatContactConflicts.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { CustomFieldMetadata, ILivechatContact, Serialized } from '@rocket.chat/core-typings';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
|
||||
const fieldNameMap: { [key: string]: TranslationKey } = {
|
||||
name: 'Name',
|
||||
contactManager: 'Contact_Manager',
|
||||
};
|
||||
|
||||
type MappedContactConflicts = Record<string, { name: string; label: string; values: string[] }>;
|
||||
|
||||
export function mapLivechatContactConflicts(
|
||||
contact: Serialized<ILivechatContact>,
|
||||
metadata: CustomFieldMetadata[] = [],
|
||||
): MappedContactConflicts {
|
||||
if (!contact.conflictingFields?.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const conflicts = contact.conflictingFields.reduce((acc, current) => {
|
||||
const fieldName = current.field === 'manager' ? 'contactManager' : current.field.replace('customFields.', '');
|
||||
|
||||
if (acc[fieldName]) {
|
||||
acc[fieldName].values.push(current.value);
|
||||
} else {
|
||||
acc[fieldName] = {
|
||||
name: fieldName,
|
||||
label:
|
||||
(current.field.startsWith('customFields.') && metadata.find(({ name }) => name === fieldName)?.label) || fieldNameMap[fieldName],
|
||||
values: [current.value],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {} as MappedContactConflicts);
|
||||
|
||||
// If there's a name conflict, add the current name to the conflict values as well
|
||||
if (conflicts.name?.values.length && contact.name) {
|
||||
conflicts.name.values.push(contact.name);
|
||||
}
|
||||
|
||||
// If there's a manager conflict, add the current manager to the conflict values as well
|
||||
if (conflicts.contactManager?.values.length && contact.contactManager) {
|
||||
conflicts.contactManager.values.push(contact.contactManager);
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
22
apps/meteor/lib/oauthRedirectUriServer.ts
Normal file
22
apps/meteor/lib/oauthRedirectUriServer.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { OAuth } from 'meteor/oauth';
|
||||
|
||||
import { SystemLogger } from '../server/lib/logger/system';
|
||||
|
||||
const { _redirectUri } = OAuth;
|
||||
|
||||
OAuth._redirectUri = (serviceName, config, params, absoluteUrlOptions) => {
|
||||
const ret = _redirectUri(serviceName, config, params, absoluteUrlOptions);
|
||||
|
||||
// DEPRECATED: Remove in v5.0.0
|
||||
// Meteor 2.3 removed ?close from redirect uri so we need to add it back to not break old oauth clients
|
||||
// https://github.com/meteor/meteor/commit/b5b7306bedc3e8eb241e64efb1e281925aa75dd3#diff-59244f4e0176cb1beed2e287924e97dc7ae2c0cc51494ce121a85d8937d116a5L11
|
||||
if (!config?.loginStyle && !ret.includes('close')) {
|
||||
SystemLogger.warn(
|
||||
`Automatically added ?close to 'redirect_uri' for ${serviceName}, this behavior will be removed in v5.0.0.\n` +
|
||||
"Please update your OAuth config to accept both with and without ?close as the 'redirect_uri'.",
|
||||
);
|
||||
return `${ret + (ret.includes('?') ? '&' : '?')}close`;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
131
apps/meteor/lib/publishFields.ts
Normal file
131
apps/meteor/lib/publishFields.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
export const subscriptionFields = {
|
||||
t: 1,
|
||||
ts: 1,
|
||||
ls: 1,
|
||||
lr: 1,
|
||||
name: 1,
|
||||
fname: 1,
|
||||
rid: 1,
|
||||
code: 1,
|
||||
f: 1,
|
||||
u: 1,
|
||||
category: 1,
|
||||
open: 1,
|
||||
alert: 1,
|
||||
roles: 1,
|
||||
unread: 1,
|
||||
prid: 1,
|
||||
customFields: 1,
|
||||
userMentions: 1,
|
||||
groupMentions: 1,
|
||||
archived: 1,
|
||||
audioNotificationValue: 1,
|
||||
desktopNotifications: 1,
|
||||
mobilePushNotifications: 1,
|
||||
emailNotifications: 1,
|
||||
desktopPrefOrigin: 1,
|
||||
mobilePrefOrigin: 1,
|
||||
emailPrefOrigin: 1,
|
||||
unreadAlert: 1,
|
||||
_updatedAt: 1,
|
||||
blocked: 1,
|
||||
blocker: 1,
|
||||
autoTranslate: 1,
|
||||
autoTranslateLanguage: 1,
|
||||
disableNotifications: 1,
|
||||
hideUnreadStatus: 1,
|
||||
hideMentionStatus: 1,
|
||||
muteGroupMentions: 1,
|
||||
ignored: 1,
|
||||
E2EKey: 1,
|
||||
E2ESuggestedKey: 1,
|
||||
oldRoomKeys: 1,
|
||||
draft: 1,
|
||||
threadDrafts: 1,
|
||||
tunread: 1,
|
||||
tunreadGroup: 1,
|
||||
tunreadUser: 1,
|
||||
status: 1,
|
||||
inviter: 1,
|
||||
|
||||
// Omnichannel fields
|
||||
department: 1,
|
||||
v: 1,
|
||||
onHold: 1,
|
||||
} as const;
|
||||
|
||||
export const roomFields = {
|
||||
_id: 1,
|
||||
name: 1,
|
||||
fname: 1,
|
||||
t: 1,
|
||||
cl: 1,
|
||||
u: 1,
|
||||
lm: 1,
|
||||
// usernames: 1,
|
||||
teamId: 1,
|
||||
teamMain: 1,
|
||||
topic: 1,
|
||||
announcement: 1,
|
||||
announcementDetails: 1,
|
||||
muted: 1,
|
||||
unmuted: 1,
|
||||
_updatedAt: 1,
|
||||
archived: 1,
|
||||
description: 1,
|
||||
default: 1,
|
||||
customFields: 1,
|
||||
lastMessage: 1,
|
||||
retention: 1,
|
||||
prid: 1,
|
||||
avatarETag: 1,
|
||||
usersCount: 1,
|
||||
msgs: 1,
|
||||
|
||||
// @TODO create an API to register this fields based on room type
|
||||
tags: 1,
|
||||
sms: 1,
|
||||
code: 1,
|
||||
joinCodeRequired: 1,
|
||||
open: 1,
|
||||
label: 1,
|
||||
ro: 1,
|
||||
reactWhenReadOnly: 1,
|
||||
sysMes: 1,
|
||||
sentiment: 1,
|
||||
broadcast: 1,
|
||||
encrypted: 1,
|
||||
e2eKeyId: 1,
|
||||
|
||||
// Omnichannel fields
|
||||
livechatData: 1,
|
||||
priorityId: 1,
|
||||
priorityWeight: 1,
|
||||
slaId: 1,
|
||||
estimatedWaitingTimeQueue: 1,
|
||||
v: 1,
|
||||
contactId: 1,
|
||||
verified: 1,
|
||||
departmentId: 1,
|
||||
servedBy: 1,
|
||||
source: 1,
|
||||
transcriptRequest: 1,
|
||||
onHold: 1,
|
||||
metrics: 1,
|
||||
ts: 1,
|
||||
waitingResponse: 1,
|
||||
queuedAt: 1,
|
||||
|
||||
// Federation fields
|
||||
federated: 1,
|
||||
federation: 1,
|
||||
|
||||
// fields used by DMs
|
||||
usernames: 1,
|
||||
uids: 1,
|
||||
|
||||
usersWaitingForE2EKeys: 1,
|
||||
|
||||
// ABAC fields
|
||||
abacAttributes: 1,
|
||||
} as const;
|
||||
6
apps/meteor/lib/renderableImageTypes.ts
Normal file
6
apps/meteor/lib/renderableImageTypes.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const unrenderableImageTypes = ['image/tiff', 'image/x-motion-jpeg', 'image/avs-video', 'image/pic'];
|
||||
|
||||
export const isRenderableImageType = (contentType?: string | null): boolean => {
|
||||
const type = (contentType || '').split(';')[0].trim().toLowerCase();
|
||||
return type.startsWith('image/') && !unrenderableImageTypes.includes(type);
|
||||
};
|
||||
18
apps/meteor/lib/roles/calculateRoomRolePriorityFromRoles.ts
Normal file
18
apps/meteor/lib/roles/calculateRoomRolePriorityFromRoles.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { IRole } from '@rocket.chat/core-typings';
|
||||
import { ROOM_ROLE_PRIORITY_MAP } from '@rocket.chat/core-typings';
|
||||
|
||||
/**
|
||||
* Retrieves the role priority for a given role.
|
||||
* @param role The role ID.
|
||||
* @returns The priority of the role.
|
||||
*/
|
||||
export const getRoomRolePriorityForRole = (role: IRole['_id']): number =>
|
||||
ROOM_ROLE_PRIORITY_MAP[role as keyof typeof ROOM_ROLE_PRIORITY_MAP] ?? ROOM_ROLE_PRIORITY_MAP.default;
|
||||
|
||||
/**
|
||||
* Calculates the minimum role priority from a list of roles.
|
||||
* @param roles The array of role IDs.
|
||||
* @returns The minimum role priority.
|
||||
*/
|
||||
export const calculateRoomRolePriorityFromRoles = (roles: IRole['_id'][]): number =>
|
||||
roles.reduce((currentMin, role) => Math.min(currentMin, getRoomRolePriorityForRole(role)), ROOM_ROLE_PRIORITY_MAP.default);
|
||||
51
apps/meteor/lib/roles/getMostImportantRole.ts
Normal file
51
apps/meteor/lib/roles/getMostImportantRole.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { IRole } from '@rocket.chat/core-typings';
|
||||
|
||||
type PriorityRoleName =
|
||||
'custom-role' | 'admin' | 'livechat-manager' | 'livechat-monitor' | 'livechat-agent' | 'user' | 'app' | 'bot' | 'guest' | 'anonymous';
|
||||
|
||||
const order = [
|
||||
'admin',
|
||||
'livechat-manager',
|
||||
'livechat-monitor',
|
||||
'livechat-agent',
|
||||
'custom-role',
|
||||
'user',
|
||||
'app',
|
||||
'bot',
|
||||
'guest',
|
||||
'anonymous',
|
||||
] as const;
|
||||
|
||||
const rolesToConsiderAsUser = ['auditor', 'auditor-log'];
|
||||
|
||||
export function getMostImportantRole(roles: IRole['_id'][] = []): 'no-role' | PriorityRoleName {
|
||||
if (!roles.length) {
|
||||
return 'no-role';
|
||||
}
|
||||
|
||||
roles = roles.map((r) => (rolesToConsiderAsUser.includes(r) ? 'user' : r));
|
||||
|
||||
if (roles.length === 1) {
|
||||
if (!(order as readonly string[]).includes(roles[0])) {
|
||||
return 'custom-role';
|
||||
}
|
||||
return roles[0] as PriorityRoleName;
|
||||
}
|
||||
|
||||
const newRoles: PriorityRoleName[] = [];
|
||||
for (const role of roles) {
|
||||
if ((order as readonly string[]).includes(role)) {
|
||||
newRoles.push(role as PriorityRoleName);
|
||||
} else if (!newRoles.includes('custom-role')) {
|
||||
newRoles.push('custom-role');
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of order) {
|
||||
if (newRoles.includes(item)) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return 'no-role';
|
||||
}
|
||||
3
apps/meteor/lib/roles/isValidRoleScope.ts
Normal file
3
apps/meteor/lib/roles/isValidRoleScope.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { IRole } from '@rocket.chat/core-typings';
|
||||
|
||||
export const isValidRoleScope = (scope: IRole['scope']): boolean => ['Users', 'Subscriptions'].includes(scope);
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://rocket.chat/schemas/room-toolbox-layout/v1.json",
|
||||
"title": "Rocket.Chat Room Toolbox Layout configuration",
|
||||
"description": "A list of entries, each declaring the toolbox layout for the room types it claims. Room types left out keep the default layout.",
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/entry" },
|
||||
"allOf": [
|
||||
{
|
||||
"$comment": "A room type may be claimed by at most one entry; minContains 0 keeps entries that do not claim it valid.",
|
||||
"contains": { "$ref": "#/$defs/entryClaimingPublicChannel" },
|
||||
"minContains": 0,
|
||||
"maxContains": 1
|
||||
},
|
||||
{
|
||||
"contains": { "$ref": "#/$defs/entryClaimingPrivateChannel" },
|
||||
"minContains": 0,
|
||||
"maxContains": 1
|
||||
},
|
||||
{
|
||||
"contains": { "$ref": "#/$defs/entryClaimingDirectMessage" },
|
||||
"minContains": 0,
|
||||
"maxContains": 1
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"roomType": { "enum": ["c", "p", "d"] },
|
||||
"entry": {
|
||||
"type": "object",
|
||||
"required": ["roomType"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"roomType": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/roomType" }
|
||||
},
|
||||
"maxVisibleNormal": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"description": "How many non-featured actions stay in the toolbox before overflowing into the kebab menu"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/item" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "minLength": 1 },
|
||||
"featured": { "type": "boolean" },
|
||||
"order": { "type": "integer" }
|
||||
}
|
||||
},
|
||||
"entryClaimingPublicChannel": {
|
||||
"type": "object",
|
||||
"required": ["roomType"],
|
||||
"properties": { "roomType": { "type": "array", "contains": { "const": "c" } } }
|
||||
},
|
||||
"entryClaimingPrivateChannel": {
|
||||
"type": "object",
|
||||
"required": ["roomType"],
|
||||
"properties": { "roomType": { "type": "array", "contains": { "const": "p" } } }
|
||||
},
|
||||
"entryClaimingDirectMessage": {
|
||||
"type": "object",
|
||||
"required": ["roomType"],
|
||||
"properties": { "roomType": { "type": "array", "contains": { "const": "d" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
34
apps/meteor/lib/rooms/adminFields.ts
Normal file
34
apps/meteor/lib/rooms/adminFields.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
|
||||
export const adminFields: Partial<Record<keyof IRoom, 1>> = {
|
||||
_id: 1,
|
||||
prid: 1,
|
||||
fname: 1,
|
||||
name: 1,
|
||||
t: 1,
|
||||
cl: 1,
|
||||
u: 1,
|
||||
usernames: 1,
|
||||
ts: 1,
|
||||
usersCount: 1,
|
||||
muted: 1,
|
||||
unmuted: 1,
|
||||
ro: 1,
|
||||
reactWhenReadOnly: 1,
|
||||
default: 1,
|
||||
favorite: 1,
|
||||
featured: 1,
|
||||
topic: 1,
|
||||
msgs: 1,
|
||||
archived: 1,
|
||||
teamId: 1,
|
||||
teamMain: 1,
|
||||
announcement: 1,
|
||||
description: 1,
|
||||
customFields: 1,
|
||||
broadcast: 1,
|
||||
uids: 1,
|
||||
avatarETag: 1,
|
||||
federated: 1,
|
||||
abacAttributes: 1,
|
||||
} as const;
|
||||
106
apps/meteor/lib/rooms/coordinator.ts
Normal file
106
apps/meteor/lib/rooms/coordinator.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { RoomType } from '@rocket.chat/core-typings';
|
||||
import type { LocationPathname } from '@rocket.chat/ui-contexts';
|
||||
|
||||
import type {
|
||||
IRoomTypeConfig,
|
||||
IRoomTypeClientDirectives,
|
||||
IRoomTypeServerDirectives,
|
||||
RoomIdentification,
|
||||
} from '../../definition/IRoomTypeConfig';
|
||||
|
||||
export abstract class RoomCoordinator {
|
||||
protected roomTypes: Record<string, { config: IRoomTypeConfig; directives: IRoomTypeClientDirectives | IRoomTypeServerDirectives }> = {};
|
||||
|
||||
protected validateRoomConfig(roomConfig: IRoomTypeConfig): void {
|
||||
const { identifier } = roomConfig;
|
||||
|
||||
if (typeof identifier !== 'string' || identifier.length === 0) {
|
||||
throw new Error('The identifier must be a string.');
|
||||
}
|
||||
}
|
||||
|
||||
protected addRoomType(roomConfig: IRoomTypeConfig, directives: IRoomTypeClientDirectives | IRoomTypeServerDirectives): void {
|
||||
this.validateRoomConfig(roomConfig);
|
||||
|
||||
if (this.roomTypes[roomConfig.identifier]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.roomTypes[roomConfig.identifier] = { config: roomConfig, directives };
|
||||
}
|
||||
|
||||
protected getRoomTypeConfig(identifier: RoomType): IRoomTypeConfig & Pick<Required<IRoomTypeConfig>, 'route'>;
|
||||
|
||||
protected getRoomTypeConfig(identifier: string): IRoomTypeConfig | undefined;
|
||||
|
||||
protected getRoomTypeConfig(identifier: string): IRoomTypeConfig | undefined {
|
||||
if (!this.roomTypes[identifier]) {
|
||||
throw new Error(`Room type with identifier ${identifier} does not exist.`);
|
||||
}
|
||||
|
||||
return this.roomTypes[identifier].config;
|
||||
}
|
||||
|
||||
public getRouteLink(roomType: string, subData: RoomIdentification): LocationPathname | false {
|
||||
const config = this.getRoomTypeConfig(roomType);
|
||||
if (!config?.route) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const routeData = this.getRouteData(roomType, subData);
|
||||
if (!routeData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!config.route.path) {
|
||||
console.warn(`The route for the room type ${roomType} does not have a path`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const pathDef = config.route.path;
|
||||
const fields = routeData;
|
||||
|
||||
const regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g;
|
||||
let path = pathDef.replace(regExp, (key) => {
|
||||
const firstRegexpChar = key.indexOf('(');
|
||||
// get the content behind : and (\\d+/)
|
||||
key = key.substring(1, firstRegexpChar > 0 ? firstRegexpChar : undefined);
|
||||
// remove +?*
|
||||
key = key.replace(/[\+\*\?]+/g, '');
|
||||
|
||||
return fields[key] || '';
|
||||
});
|
||||
|
||||
path = path.replace(/\/\/+/g, '/'); // Replace multiple slashes with single slash
|
||||
|
||||
// remove trailing slash
|
||||
// but keep the root slash if it's the only one
|
||||
path = path.match(/^\/{1}$/) ? path : path.replace(/\/$/, '');
|
||||
|
||||
return path as LocationPathname;
|
||||
}
|
||||
|
||||
protected getRouteData(roomType: string, subData: RoomIdentification): Record<string, string> | false {
|
||||
if (!subData.rid && (subData as Record<string, string>)._id) {
|
||||
console.warn('Deprecated: RoomCoordinator.getRouteData received a room object');
|
||||
subData.rid = (subData as Record<string, string>)._id;
|
||||
}
|
||||
|
||||
const config = this.getRoomTypeConfig(roomType);
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let routeData = {};
|
||||
if (config.route?.link) {
|
||||
routeData = config.route.link(subData);
|
||||
} else if (subData?.name) {
|
||||
routeData = {
|
||||
rid: subData.rid,
|
||||
name: subData.name,
|
||||
};
|
||||
}
|
||||
|
||||
return routeData;
|
||||
}
|
||||
}
|
||||
8
apps/meteor/lib/rooms/roomTypes/conversation.ts
Normal file
8
apps/meteor/lib/rooms/roomTypes/conversation.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
export function getConversationRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'merged',
|
||||
};
|
||||
}
|
||||
24
apps/meteor/lib/rooms/roomTypes/direct.ts
Normal file
24
apps/meteor/lib/rooms/roomTypes/direct.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
declare module '@rocket.chat/ui-contexts' {
|
||||
export interface IRouterPaths {
|
||||
direct: {
|
||||
pathname: `/direct/:rid${`/${string}` | ''}${`/${string}` | ''}`;
|
||||
pattern: '/direct/:rid/:tab?/:context?';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getDirectMessageRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'd',
|
||||
route: {
|
||||
name: 'direct',
|
||||
path: '/direct/:rid/:tab?/:context?',
|
||||
link(sub): Record<string, string> {
|
||||
return { rid: sub.rid || sub.name || '' };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
8
apps/meteor/lib/rooms/roomTypes/favorite.ts
Normal file
8
apps/meteor/lib/rooms/roomTypes/favorite.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
export function getFavoriteRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'f',
|
||||
};
|
||||
}
|
||||
24
apps/meteor/lib/rooms/roomTypes/livechat.ts
Normal file
24
apps/meteor/lib/rooms/roomTypes/livechat.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
declare module '@rocket.chat/ui-contexts' {
|
||||
export interface IRouterPaths {
|
||||
live: {
|
||||
pathname: `/live/${string}${`/${string}` | ''}${`/${string}` | ''}`;
|
||||
pattern: '/live/:id/:tab?/:context?';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getLivechatRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'l',
|
||||
route: {
|
||||
name: 'live',
|
||||
path: '/live/:id/:tab?/:context?',
|
||||
link({ rid, tab }): Record<string, string> {
|
||||
return { id: rid || '', tab: tab ?? 'room-info' };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
21
apps/meteor/lib/rooms/roomTypes/private.ts
Normal file
21
apps/meteor/lib/rooms/roomTypes/private.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
declare module '@rocket.chat/ui-contexts' {
|
||||
export interface IRouterPaths {
|
||||
group: {
|
||||
pathname: `/group/${string}${`/${string}` | ''}${`/${string}` | ''}`;
|
||||
pattern: '/group/:name/:tab?/:context?';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getPrivateRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'p',
|
||||
route: {
|
||||
name: 'group',
|
||||
path: '/group/:name/:tab?/:context?',
|
||||
},
|
||||
};
|
||||
}
|
||||
21
apps/meteor/lib/rooms/roomTypes/public.ts
Normal file
21
apps/meteor/lib/rooms/roomTypes/public.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
declare module '@rocket.chat/ui-contexts' {
|
||||
export interface IRouterPaths {
|
||||
channel: {
|
||||
pathname: `/channel/${string}${`/${string}` | ''}${`/${string}` | ''}`;
|
||||
pattern: '/channel/:name/:tab?/:context?';
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getPublicRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'c',
|
||||
route: {
|
||||
name: 'channel',
|
||||
path: '/channel/:name/:tab?/:context?',
|
||||
},
|
||||
};
|
||||
}
|
||||
8
apps/meteor/lib/rooms/roomTypes/unread.ts
Normal file
8
apps/meteor/lib/rooms/roomTypes/unread.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { IRoomTypeConfig } from '../../../definition/IRoomTypeConfig';
|
||||
import type { RoomCoordinator } from '../coordinator';
|
||||
|
||||
export function getUnreadRoomType(_coordinator: RoomCoordinator): IRoomTypeConfig {
|
||||
return {
|
||||
identifier: 'unread',
|
||||
};
|
||||
}
|
||||
24
apps/meteor/lib/transforms.ts
Normal file
24
apps/meteor/lib/transforms.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type Transform<T, U extends T | Promise<Awaited<T>>> = (x: T) => U;
|
||||
|
||||
type TransformChain<T, U extends T | Promise<Awaited<T>>> = {
|
||||
(x: T): U;
|
||||
use(transform: Transform<T, U>): () => void;
|
||||
};
|
||||
|
||||
export const createAsyncTransformChain = <T>(
|
||||
...transforms: Transform<T, Promise<Awaited<T>>>[]
|
||||
): TransformChain<T, Promise<Awaited<T>>> => {
|
||||
let chain = transforms;
|
||||
|
||||
const call = (x: T): Promise<Awaited<T>> => chain.reduce((x, transform) => x.then(transform), Promise.resolve(x));
|
||||
|
||||
const use = (transform: Transform<T, Promise<Awaited<T>>>): (() => void) => {
|
||||
chain.push(transform);
|
||||
|
||||
return (): void => {
|
||||
chain = chain.filter((t) => t !== transform);
|
||||
};
|
||||
};
|
||||
|
||||
return Object.assign(call, { use });
|
||||
};
|
||||
4
apps/meteor/lib/utils/addMinutesToADate.ts
Normal file
4
apps/meteor/lib/utils/addMinutesToADate.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const addMinutesToADate = (date: Date, minutes: number): Date => {
|
||||
const minutesInMs = minutes * 60 * 1000;
|
||||
return new Date(date.getTime() + minutesInMs);
|
||||
};
|
||||
4
apps/meteor/lib/utils/arrayUtils.ts
Normal file
4
apps/meteor/lib/utils/arrayUtils.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function ensureArray<T>(param: T | Array<T>): Array<T> {
|
||||
const emptyArray: Array<T> = [];
|
||||
return emptyArray.concat(param);
|
||||
}
|
||||
7
apps/meteor/lib/utils/closeUnclosedCodeBlock.ts
Normal file
7
apps/meteor/lib/utils/closeUnclosedCodeBlock.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const closeUnclosedCodeBlock = (text: string): string => {
|
||||
const backtickCount = (text.match(/```/g) || []).length;
|
||||
if (backtickCount % 2 !== 0) {
|
||||
return `${text}\n\`\`\``;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
5
apps/meteor/lib/utils/comparisons.ts
Normal file
5
apps/meteor/lib/utils/comparisons.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const compareByRanking =
|
||||
<T>(rank: (x: T) => number) =>
|
||||
(a: T, b: T) => {
|
||||
return rank(a) - rank(b);
|
||||
};
|
||||
3
apps/meteor/lib/utils/exhaustiveCheck.ts
Normal file
3
apps/meteor/lib/utils/exhaustiveCheck.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const exhaustiveCheck = (_: never): never => {
|
||||
throw new Error('Exhaustive check failed');
|
||||
};
|
||||
13
apps/meteor/lib/utils/generatePath.ts
Normal file
13
apps/meteor/lib/utils/generatePath.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { compile } from 'path-to-regexp';
|
||||
|
||||
type PathParams<TPath extends string> = TPath extends `${string}:${infer TParam}/${infer TRest}`
|
||||
? (TParam extends `${infer U}?` ? U : TParam) | PathParams<TRest>
|
||||
: TPath extends `${string}:${infer TParam}`
|
||||
? TParam extends `${infer U}?`
|
||||
? U
|
||||
: TParam
|
||||
: never;
|
||||
|
||||
export function generatePath<TPath extends string>(path: TPath, params?: Partial<Record<PathParams<TPath>, string>>): string {
|
||||
return compile(path, { encode: encodeURIComponent })(params);
|
||||
}
|
||||
13
apps/meteor/lib/utils/getFileExtension.ts
Normal file
13
apps/meteor/lib/utils/getFileExtension.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const getFileExtension = (fileName?: string): string => {
|
||||
if (!fileName) {
|
||||
return 'file';
|
||||
}
|
||||
|
||||
const arr = fileName.split('.');
|
||||
|
||||
if (arr.length < 2 || (arr[0] === '' && arr.length === 2)) {
|
||||
return 'file';
|
||||
}
|
||||
|
||||
return arr.pop()?.toLocaleUpperCase() || 'file';
|
||||
};
|
||||
69
apps/meteor/lib/utils/highOrderFunctions.ts
Normal file
69
apps/meteor/lib/utils/highOrderFunctions.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export const withThrottling =
|
||||
({ wait, leading, trailing }: { wait: number; leading?: boolean; trailing?: boolean }) =>
|
||||
<TFunction extends (...args: any[]) => any>(fn: TFunction) => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let result: ReturnType<TFunction>;
|
||||
let previous = 0;
|
||||
|
||||
function throttled(this: ThisParameterType<TFunction>, ...args: Parameters<TFunction>) {
|
||||
const now = Date.now();
|
||||
if (!previous && leading === false) previous = now;
|
||||
const remaining = wait - (now - previous);
|
||||
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = undefined;
|
||||
previous = now;
|
||||
result = fn.apply(this, args);
|
||||
} else if (!timer && trailing !== false) {
|
||||
const later = () => {
|
||||
previous = leading === false ? 0 : Date.now();
|
||||
result = fn.apply(this, args);
|
||||
timer = undefined;
|
||||
};
|
||||
timer = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
previous = 0;
|
||||
timer = undefined;
|
||||
};
|
||||
|
||||
return Object.assign(throttled, { cancel });
|
||||
};
|
||||
|
||||
export const withDebouncing =
|
||||
({ wait, immediate }: { wait: number; immediate?: boolean }) =>
|
||||
<TFunction extends (...args: any[]) => any>(fn: TFunction) => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
|
||||
let result: ReturnType<TFunction>;
|
||||
let previous: number;
|
||||
|
||||
function debounced(this: ThisParameterType<TFunction>, ...args: Parameters<TFunction>) {
|
||||
previous = Date.now();
|
||||
if (!timer) {
|
||||
const later = () => {
|
||||
const passed = Date.now() - previous;
|
||||
if (wait > passed) {
|
||||
timer = setTimeout(later, wait - passed);
|
||||
} else {
|
||||
timer = undefined;
|
||||
if (!immediate) result = fn.apply(this, args);
|
||||
}
|
||||
};
|
||||
timer = setTimeout(later, wait);
|
||||
if (immediate) result = fn.apply(this, args);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
};
|
||||
|
||||
return Object.assign(debounced, { cancel });
|
||||
};
|
||||
25
apps/meteor/lib/utils/isJSON.spec.ts
Normal file
25
apps/meteor/lib/utils/isJSON.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { isJSON } from './isJSON';
|
||||
|
||||
describe('isJSON', () => {
|
||||
it('returns true for any successfully parsed JSON', () => {
|
||||
expect(isJSON('{"a":1}')).toBe(true);
|
||||
expect(isJSON('{}')).toBe(true);
|
||||
expect(isJSON('[1,2]')).toBe(true);
|
||||
expect(isJSON('"hello"')).toBe(true);
|
||||
expect(isJSON('true')).toBe(true);
|
||||
expect(isJSON('42')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for valid but falsy JSON values', () => {
|
||||
expect(isJSON('false')).toBe(true);
|
||||
expect(isJSON('null')).toBe(true);
|
||||
expect(isJSON('0')).toBe(true);
|
||||
expect(isJSON('""')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for malformed JSON', () => {
|
||||
expect(isJSON('{ not json }')).toBe(false);
|
||||
expect(isJSON('{"a":')).toBe(false);
|
||||
expect(isJSON('')).toBe(false);
|
||||
});
|
||||
});
|
||||
8
apps/meteor/lib/utils/isJSON.ts
Normal file
8
apps/meteor/lib/utils/isJSON.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const isJSON = (value: string): boolean => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
7
apps/meteor/lib/utils/isObject.ts
Normal file
7
apps/meteor/lib/utils/isObject.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @deprecated use isObject from tools package instead
|
||||
*/
|
||||
export function isObject(obj: unknown): obj is object {
|
||||
const type = typeof obj;
|
||||
return type === 'function' || (type === 'object' && !!obj);
|
||||
}
|
||||
3
apps/meteor/lib/utils/isPlainObject.ts
Normal file
3
apps/meteor/lib/utils/isPlainObject.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isPlainObject(value: unknown): value is Record<string | symbol, unknown> {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
1
apps/meteor/lib/utils/isRelativeURL.ts
Normal file
1
apps/meteor/lib/utils/isRelativeURL.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const isRelativeURL = (str: string): boolean => /^[^\/]+\/[^\/].*$|^\/[^\/].*$/gim.test(str);
|
||||
8
apps/meteor/lib/utils/normalizeUsername.ts
Normal file
8
apps/meteor/lib/utils/normalizeUsername.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Strips a leading '@' from a username if present.
|
||||
*
|
||||
* Federated usernames (e.g. @john.doe:matrix.org) are stored with a leading '@',
|
||||
* while local usernames are not. This utility normalises both forms so callers
|
||||
* that render `@{username}` don't accidentally produce `@@john.doe:matrix.org`.
|
||||
*/
|
||||
export const normalizeUsername = (username: string): string => (username.startsWith('@') ? username.slice(1) : username);
|
||||
7
apps/meteor/lib/utils/omit.ts
Normal file
7
apps/meteor/lib/utils/omit.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function omit<TObject, TKey extends keyof TObject>(obj: TObject, ...keys: TKey[]): Omit<TObject, TKey> {
|
||||
const result = { ...obj };
|
||||
keys.forEach((key) => {
|
||||
delete result[key];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
9
apps/meteor/lib/utils/parseCSV.ts
Normal file
9
apps/meteor/lib/utils/parseCSV.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function parseCSV(csv: string, removeEmptyItems = true): string[] {
|
||||
const items = csv.split(',').map((item) => item.trim());
|
||||
|
||||
if (removeEmptyItems) {
|
||||
return items.filter(Boolean);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
64
apps/meteor/lib/utils/parseParameters.ts
Normal file
64
apps/meteor/lib/utils/parseParameters.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
function split(parameters: string, ignoreExtraUnquotedSpaces = true): string[] {
|
||||
// Replace \n\r with a single \n
|
||||
const line = parameters.replace(/\n\r/gm, '\n');
|
||||
|
||||
const items = line.split(/[ \n\r\t]/);
|
||||
|
||||
if (ignoreExtraUnquotedSpaces) {
|
||||
return items.filter(Boolean);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function parseParameters(parameters: string, ignoreExtraUnquotedSpaces = true): string[] {
|
||||
if (!parameters.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const match = parameters.match(/((["'])(?:(?=(\\?))\3.)*?\2)/gs);
|
||||
let line = parameters;
|
||||
|
||||
if (!match) {
|
||||
return split(line, ignoreExtraUnquotedSpaces);
|
||||
}
|
||||
|
||||
match.forEach((item) => {
|
||||
const newItem = item
|
||||
// Replace start quote with SSA character
|
||||
.replace(/(^['"])/g, '\u0086')
|
||||
// Replace end quote with ESA character
|
||||
.replace(/['"]$/g, '\u0087')
|
||||
// Replace spaces inside the quotes with a Punctuation Space character
|
||||
.replace(/ /g, '\u2008')
|
||||
// Replace new lines inside the quotes with a PLD character
|
||||
.replace(/\n/gm, '\u008B')
|
||||
// Replace carriage returns inside the quotes with a PLU character
|
||||
.replace(/\r/gm, '\u008C')
|
||||
// Replace tabs inside the quotes with a VTS character
|
||||
.replace(/\t/g, '\u008A');
|
||||
|
||||
line = line.replace(item, newItem);
|
||||
});
|
||||
|
||||
// If two quoted parameters are not separated by a space, add one automatically
|
||||
line = line.replace(/\u0087\u0086/g, '\u0087 \u0086');
|
||||
|
||||
const items = split(line, ignoreExtraUnquotedSpaces);
|
||||
|
||||
return items.map((item) =>
|
||||
item
|
||||
// Convert back the spaces from inside quotes
|
||||
.replace(/\u2008/g, ' ')
|
||||
// Convert back the new lines from inside quotes
|
||||
.replace(/\u008B/g, '\n')
|
||||
// Convert back the carriage returns from inside quotes
|
||||
.replace(/\u008C/g, '\r')
|
||||
// Convert back the tabs from inside quotes
|
||||
.replace(/\u008A/g, '\t')
|
||||
// Remove SSA and ESA characters
|
||||
.replace(/[\u0086\u0087]/g, '')
|
||||
// Unescape quotes
|
||||
.replace(/\\\"/g, '"'),
|
||||
);
|
||||
}
|
||||
5
apps/meteor/lib/utils/safeHtmlDots.ts
Normal file
5
apps/meteor/lib/utils/safeHtmlDots.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// default email engines - like gmail - will render texts with dots as an anchor tag.
|
||||
// If we can, we should avoid that.
|
||||
export const safeHtmlDots = (text: string): string => {
|
||||
return text.replace(/\./g, '․');
|
||||
};
|
||||
14
apps/meteor/lib/utils/secondsToHHMMSS.ts
Normal file
14
apps/meteor/lib/utils/secondsToHHMMSS.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @todo check alternatives for date/time formatting
|
||||
*/
|
||||
export const secondsToHHMMSS = (sec: number | string): string => {
|
||||
if (typeof sec !== 'number') {
|
||||
sec = parseFloat(sec);
|
||||
}
|
||||
|
||||
const hours = Math.floor(sec / 3600);
|
||||
const minutes = Math.floor((sec - hours * 3600) / 60);
|
||||
const seconds = Math.round(sec - hours * 3600 - minutes * 60);
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
2
apps/meteor/lib/utils/sleep.ts
Normal file
2
apps/meteor/lib/utils/sleep.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// Promisified sleep function
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
113
apps/meteor/lib/utils/stringUtils.ts
Normal file
113
apps/meteor/lib/utils/stringUtils.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
export function truncate(str: string, length: number): string {
|
||||
return str.length > length ? `${str.slice(0, length - 3)}...` : str;
|
||||
}
|
||||
|
||||
function makeString(object: unknown): string {
|
||||
if (!object) return '';
|
||||
return String(object);
|
||||
}
|
||||
|
||||
function defaultToWhiteSpace(characters: unknown): string {
|
||||
if (!characters) return '\\s';
|
||||
if (typeof characters === 'object' && 'source' in characters) return (characters as { source: string }).source;
|
||||
return `[${escapeRegExp(makeString(characters))}]`;
|
||||
}
|
||||
|
||||
const nativeTrim = String.prototype.trim;
|
||||
const nativeTrimLeft = String.prototype.trimLeft;
|
||||
const nativeTrimRight = String.prototype.trimRight;
|
||||
|
||||
export function trim(_str: unknown, _characters?: unknown): string {
|
||||
const str = makeString(_str);
|
||||
if (!_characters && nativeTrim) return nativeTrim.call(str);
|
||||
const characters = defaultToWhiteSpace(_characters);
|
||||
return str.replace(new RegExp(`^${characters}+|${characters}+$`, 'g'), '');
|
||||
}
|
||||
|
||||
export function ltrim(_str: unknown, _characters: unknown): string {
|
||||
const str = makeString(_str);
|
||||
if (!_characters && nativeTrimLeft) return nativeTrimLeft.call(str);
|
||||
const characters = defaultToWhiteSpace(_characters);
|
||||
return str.replace(new RegExp(`^${characters}+`), '');
|
||||
}
|
||||
|
||||
export function rtrim(_str: unknown, _characters: unknown): string {
|
||||
const str = makeString(_str);
|
||||
if (!_characters && nativeTrimRight) return nativeTrimRight.call(str);
|
||||
const characters = defaultToWhiteSpace(_characters);
|
||||
return str.replace(new RegExp(`${characters}+$`), '');
|
||||
}
|
||||
|
||||
export function capitalize(_str: unknown, lowercaseRest: boolean): string {
|
||||
const str = makeString(_str);
|
||||
const remainingChars = !lowercaseRest ? str.slice(1) : str.slice(1).toLowerCase();
|
||||
|
||||
return str.charAt(0).toUpperCase() + remainingChars;
|
||||
}
|
||||
|
||||
export function stripTags(str: unknown): string {
|
||||
return DOMPurify.sanitize(makeString(str)).replace(/<\/?[^>]+>/g, '');
|
||||
}
|
||||
|
||||
export function strLeft(_str: unknown, _sep: unknown): string {
|
||||
const str = makeString(_str);
|
||||
const sep = makeString(_sep);
|
||||
const pos = !sep ? -1 : str.indexOf(sep);
|
||||
return ~pos ? str.slice(0, pos) : str;
|
||||
}
|
||||
|
||||
export function strRight(_str: unknown, _sep: unknown): string {
|
||||
const str = makeString(_str);
|
||||
const sep = makeString(_sep);
|
||||
const pos = !sep ? -1 : str.indexOf(sep);
|
||||
return ~pos ? str.slice(pos + sep.length, str.length) : str;
|
||||
}
|
||||
|
||||
export function strRightBack(_str: unknown, _sep: unknown): string {
|
||||
const str = makeString(_str);
|
||||
const sep = makeString(_sep);
|
||||
const pos = !sep ? -1 : str.lastIndexOf(sep);
|
||||
return ~pos ? str.slice(pos + sep.length, str.length) : str;
|
||||
}
|
||||
|
||||
export function numberFormat(_number: number, dec: number, dsep?: string, tsep?: string): string {
|
||||
if (isNaN(_number) || _number === null || _number === undefined) return '';
|
||||
|
||||
const number = _number.toFixed(~~dec);
|
||||
tsep = typeof tsep === 'string' ? tsep : ',';
|
||||
|
||||
const parts = number.split('.');
|
||||
const fnums = parts[0];
|
||||
const decimals = parts[1] ? (dsep || '.') + parts[1] : '';
|
||||
|
||||
return fnums.replace(/(\d)(?=(?:\d{3})+$)/g, `$1${tsep}`) + decimals;
|
||||
}
|
||||
|
||||
export function pad(_str: unknown, _length: number, padStr?: string, type: 'right' | 'left' | 'both' = 'right') {
|
||||
const str = makeString(_str);
|
||||
const length = ~~_length;
|
||||
|
||||
let padlen = 0;
|
||||
|
||||
if (!padStr) padStr = ' ';
|
||||
else if (padStr.length > 1) padStr = padStr.charAt(0);
|
||||
|
||||
switch (type) {
|
||||
case 'right':
|
||||
padlen = length - str.length;
|
||||
return str + padStr.repeat(padlen);
|
||||
case 'both':
|
||||
padlen = length - str.length;
|
||||
return padStr.repeat(Math.ceil(padlen / 2)) + str + padStr.repeat(Math.floor(padlen / 2));
|
||||
default: // 'left'
|
||||
padlen = length - str.length;
|
||||
return padStr.repeat(padlen) + str;
|
||||
}
|
||||
}
|
||||
|
||||
export function lrpad(str: unknown, length: number, padStr?: string): string {
|
||||
return pad(str, length, padStr, 'both');
|
||||
}
|
||||
20
apps/meteor/lib/utils/throttledCounter.ts
Normal file
20
apps/meteor/lib/utils/throttledCounter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { throttle } from 'underscore';
|
||||
|
||||
export function throttledCounter(fn: (counter: number) => unknown, wait: number) {
|
||||
let counter = 0;
|
||||
|
||||
const throttledFn = throttle(
|
||||
() => {
|
||||
fn(counter);
|
||||
|
||||
counter = 0;
|
||||
},
|
||||
wait,
|
||||
{ leading: false },
|
||||
);
|
||||
|
||||
return () => {
|
||||
counter++;
|
||||
throttledFn();
|
||||
};
|
||||
}
|
||||
5
apps/meteor/lib/videoConference/constants.ts
Normal file
5
apps/meteor/lib/videoConference/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const availabilityErrors = {
|
||||
NOT_CONFIGURED: 'video-conf-provider-not-configured',
|
||||
NOT_ACTIVE: 'no-active-video-conf-provider',
|
||||
NO_APP: 'no-videoconf-provider-app',
|
||||
};
|
||||
Reference in New Issue
Block a user