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
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
import server from '@rocket.chat/jest-presets/server';
import type { Config } from 'jest';
export default {
preset: server.preset,
} satisfies Config;

View File

@@ -0,0 +1,44 @@
{
"name": "@rocket.chat/core-services",
"version": "0.16.0",
"private": true,
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "rm -rf dist && tsc",
"dev": "tsc --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest",
"testunit": "jest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/federation-sdk": "0.7.0",
"@rocket.chat/http-router": "workspace:^",
"@rocket.chat/icons": "^0.49.0",
"@rocket.chat/media-signaling": "workspace:^",
"@rocket.chat/message-parser": "workspace:^",
"@rocket.chat/models": "workspace:^",
"@rocket.chat/rest-typings": "workspace:^",
"@rocket.chat/tracing": "workspace:^",
"@rocket.chat/ui-kit": "workspace:~"
},
"devDependencies": {
"@rocket.chat/apps": "workspace:^",
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/tsconfig": "workspace:*",
"@types/jest": "~30.0.0",
"eslint": "~9.39.5",
"jest": "~30.2.0",
"mongodb": "6.16.0",
"typescript": "~5.9.3"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1,231 @@
import { EventEmitter } from 'node:events';
import { Logger } from '@rocket.chat/logger';
import { InstanceStatus } from '@rocket.chat/models';
import { injectCurrentContext, tracerActiveSpan } from '@rocket.chat/tracing';
import { asyncLocalStorage } from '.';
import type { EventSignatures } from './events/Events';
import type { CallingOptions, IBroker, IBrokerNode } from './types/IBroker';
import type { ServiceClass, IServiceClass } from './types/ServiceClass';
type ExtendedServiceClass = { instance: IServiceClass; dependencies: string[]; isStarted: boolean };
const logger = new Logger('LocalBroker');
const INTERVAL = 1000;
const TIMEOUT = INTERVAL * 10;
export class LocalBroker implements IBroker {
private started = false;
private methods = new Map<string, (...params: any) => any>();
private events = new EventEmitter();
private services = new Map<string, ExtendedServiceClass>();
private pendingServices: Set<string> = new Set();
private defaultDependencies = ['settings'];
async call(method: string, data: any, options?: CallingOptions): Promise<any> {
if (options) {
logger.warn('Options are not supported in LocalBroker');
}
return tracerActiveSpan(
`action ${method}`,
{},
() => {
return asyncLocalStorage.run(
{
id: 'ctx.id',
nodeID: 'ctx.nodeID',
requestID: 'ctx.requestID',
broker: this,
},
(): any => this.methods.get(method)?.(...data),
);
},
injectCurrentContext(),
);
}
async destroyService(instance: ServiceClass): Promise<void> {
const namespace = instance.getName();
instance.getEvents().forEach((event) => event.listeners.forEach((listener) => this.events.removeListener(event.eventName, listener)));
const methods =
instance.constructor?.name === 'Object'
? Object.getOwnPropertyNames(instance)
: Object.getOwnPropertyNames(Object.getPrototypeOf(instance));
for (const method of methods) {
if (method === 'constructor') {
continue;
}
this.methods.delete(`${namespace}.${method}`);
}
instance.removeAllListeners();
await instance.stopped();
this.services.delete(namespace);
}
/**
* Creates a service and adds it to the local broker. In case of the broker is already started, it will start the service automatically.
*/
createService(instance: IServiceClass, serviceDependencies: string[] = []): void {
const serviceName = instance.getName();
if (!serviceName || serviceName === '') {
throw new Error('Service name cannot be empty');
}
if (this.services.has(serviceName)) {
throw new Error(`Service ${serviceName} already exists`);
}
// TODO: find a better way to handle default dependencies and avoid loops
const dependencies = [...serviceDependencies, ...(serviceName === 'settings' ? [] : this.defaultDependencies)].filter(
(dependency) => dependency !== serviceName,
);
void instance.created();
instance.getEvents().forEach((event) => event.listeners.forEach((listener) => this.events.on(event.eventName, listener)));
const methods =
instance.constructor?.name === 'Object'
? Object.getOwnPropertyNames(instance)
: Object.getOwnPropertyNames(Object.getPrototypeOf(instance));
for (const method of methods) {
if (method === 'constructor') {
continue;
}
const i = instance as any;
this.methods.set(`${serviceName}.${method}`, i[method].bind(i));
}
this.services.set(serviceName, { instance, dependencies, isStarted: false });
this.registerPendingServices(Array.from(new Set([serviceName, ...dependencies])));
if (this.started) {
void this.start();
}
}
onBroadcast(callback: (eventName: string, args: unknown[]) => void): void {
this.events.on('broadcast', callback);
}
async broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
void this.broadcastLocal(event, ...args);
this.events.emit('broadcast', event, args);
}
async broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
this.events.emit(event, ...args);
}
async broadcastToServices<T extends keyof EventSignatures>(
_services: string[],
event: T,
...args: Parameters<EventSignatures[T]>
): Promise<void> {
this.events.emit(event, ...args);
}
async nodeList(): Promise<IBrokerNode[]> {
// TODO models should not be called form here. we should create an abstraction to an internal service to perform this query
const instances = await InstanceStatus.find({}, { projection: { _id: 1 } }).toArray();
return instances.map(({ _id }) => ({ id: _id, available: true }));
}
/**
* Registers services to be started. We're assuming that each service will only have one level of dependencies.
*/
private registerPendingServices(services: string[] = []): void {
services
.filter((e) => !this.services.has(e) || !this.services.get(e)?.isStarted)
.forEach((service) => this.pendingServices.add(service));
}
/**
* Removes a service from the pending services set.
*/
private removePendingService(service: string): void {
this.pendingServices.delete(service);
}
private async startService(service: ExtendedServiceClass): Promise<void> {
const serviceName = service.instance.getName();
if (typeof service === 'string') {
logger.debug({ msg: 'Service is not in the services map. Bringing it back to queue', serviceName });
return;
}
if (service?.isStarted) {
logger.debug({ msg: 'Service already started', serviceName });
return;
}
const pendingDependencies = service.dependencies.filter((e) => !this.services.has(e) || !this.services.get(e)?.isStarted);
if (pendingDependencies.length > 0) {
logger.debug({
msg: 'Service has dependencies that are not started yet, bringing it back to queue',
serviceName,
pendingDependencies,
});
return;
}
await service.instance.started();
this.services.set(serviceName, { ...service, isStarted: true });
this.removePendingService(serviceName);
logger.debug({ msg: 'Service successfully started', serviceName });
}
async start(): Promise<void> {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const intervalId = setInterval(async () => {
const elapsed = Date.now() - startTime;
if (this.pendingServices.size === 0) {
const availableServices = Array.from(this.services.values()).filter((service) => service.isStarted);
logger.info({ msg: 'All services available', count: availableServices.length });
clearInterval(intervalId);
return resolve();
}
if (elapsed > TIMEOUT) {
clearInterval(intervalId);
const pendingServices = Array.from(this.pendingServices).join(', ');
const err = new Error(`Timeout while waiting for LocalBroker services: ${pendingServices}`);
logger.error({ msg: 'Timeout while waiting for LocalBroker services', err, pendingServices });
return reject(err);
}
for (const service of Array.from(this.pendingServices)) {
const serviceInstance = this.services.get(service);
if (serviceInstance) {
await this.startService(serviceInstance);
}
}
logger.debug({ msg: 'Waiting for pending services', count: this.pendingServices.size });
}, INTERVAL);
});
}
}

View File

@@ -0,0 +1,26 @@
export class MeteorError extends Error {
public isClientSafe = true;
public readonly errorType = 'Meteor.Error';
public constructor(
public readonly error: string | number,
public readonly reason?: string,
public readonly details?: any,
) {
super(`${reason ? `${reason} ` : ''}[${String(error)}]`);
}
public toJSON(): any {
return {
isClientSafe: this.isClientSafe,
errorType: this.errorType,
error: this.error,
reason: this.reason,
message: this.message,
...(this.details && { details: this.details }),
};
}
}
export const isMeteorError = (error: any): error is MeteorError => error?.errorType === 'Meteor.Error';

View File

@@ -0,0 +1,4 @@
import { Api } from './lib/Api';
// Singleton instance of the Service API
export const api = new Api();

View File

@@ -0,0 +1,309 @@
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings';
import type {
IEmailInbox,
IEmoji,
IInstanceStatus,
IIntegration,
IIntegrationHistory,
ILivechatDepartmentAgents,
ILoginServiceConfiguration,
IMessage,
INotificationDesktop,
IRole,
IRoom,
ISetting,
ISocketConnection,
ISubscription,
IUser,
IInvite,
ICustomSound,
UserStatus,
ILivechatPriority,
VideoConference,
ICalendarNotification,
AtLeast,
ILivechatInquiryRecord,
ILivechatAgent,
IBanner,
ILivechatVisitor,
LicenseLimitKind,
ICustomUserStatus,
IWebdavAccount,
MessageAttachment,
} from '@rocket.chat/core-typings';
import type { ClientMediaSignalBody, ServerMediaSignal } from '@rocket.chat/media-signaling';
import type * as UiKit from '@rocket.chat/ui-kit';
import type { AutoUpdateRecord } from '../types/IMeteor';
export type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed';
type LoginServiceConfigurationEvent = {
id: string;
} & (
| {
clientAction: 'removed';
data?: never;
}
| {
clientAction: Omit<ClientAction, 'removed'>;
data: Omit<Partial<ILoginServiceConfiguration>, 'secret'> & { secret?: never };
}
);
export type EventSignatures = {
'room.video-conference': (params: { rid: string; callId: string }) => void;
'shutdown': (params: Record<string, string[]>) => void;
'$services.changed': (info: { localService: boolean }) => void;
'accounts.login': (info: { userId: string; connection: ISocketConnection }) => void;
'accounts.logout': (info: { userId: string; connection: ISocketConnection }) => void;
'authorization.guestPermissions': (permissions: string[]) => void;
'socket.connected': (connection: ISocketConnection) => void;
'socket.disconnected': (connection: ISocketConnection) => void;
'banner.new'(bannerId: string): void;
'banner.enabled'(bannerId: string): void;
'banner.disabled'(bannerId: string): void;
'banner.user'(userId: string, banner: IBanner): void;
'emoji.deleteCustom'(emoji: IEmoji): void;
'emoji.updateCustom'(emoji: IEmoji): void;
'license.module'(data: { module: string; valid: boolean }): void;
'license.sync'(): void;
'license.actions'(actions: Record<Partial<LicenseLimitKind>, boolean>): void;
'livechat-inquiry-queue-observer'(data: { action: string; inquiry: ILivechatInquiryRecord }): void;
'message'(data: { action: string; message: IMessage }): void;
'meteor.clientVersionUpdated'(data: AutoUpdateRecord): void;
'notify.desktop'(uid: string, data: INotificationDesktop): void;
'notify.uiInteraction'(uid: string, data: UiKit.ServerInteraction): void;
'notify.updateInvites'(uid: string, data: { invite: Omit<IInvite, '_updatedAt'> }): void;
'notify.ephemeralMessage'(uid: string, rid: string, message: AtLeast<IMessage, 'msg'>): void;
'notify.webdav'(
uid: string,
data:
| {
type: 'changed';
account: Partial<IWebdavAccount>;
}
| {
type: 'removed';
account: { _id: IWebdavAccount['_id'] };
},
): void;
'notify.e2e.keyRequest'(rid: string, data: IRoom['e2eKeyId']): void;
'notify.deleteMessage'(rid: string, data: { _id: string }): void;
'notify.deleteMessageBulk'(
rid: string,
data: {
rid: string;
excludePinned: boolean;
ignoreDiscussion: boolean;
ts: Record<string, Date>;
users: string[];
ids?: string[]; // message ids have priority over ts
showDeletedStatus?: boolean;
} & (
| {
filesOnly: true;
replaceFileAttachmentsWith?: MessageAttachment;
}
| {
filesOnly?: false;
}
),
): void;
'notify.deleteCustomSound'(data: { soundData: ICustomSound }): void;
'notify.updateCustomSound'(data: { soundData: ICustomSound }): void;
'notify.calendar'(uid: string, data: ICalendarNotification): void;
'notify.messagesRead'(data: { rid: string; until: Date; tmid?: string }): void;
'notify.importedMessages'(data: { roomIds: string[] }): void;
'permission.changed'(data: { clientAction: ClientAction; data: any }): void;
'room'(data: { action: string; room: Partial<IRoom> }): void;
'room.avatarUpdate'(room: Pick<IRoom, '_id' | 'avatarETag'>): void;
'setting'(data: { action: string; setting: Partial<ISetting> }): void;
'stream'([streamer, eventName, payload]: [string, string, any[]]): void;
'subscription'(data: { action: string; subscription: Partial<ISubscription> }): void;
'user.avatarUpdate'(user: Partial<IUser>): void;
'user.deleted'(
user: Pick<IUser, '_id'>,
data:
| {
messageErasureType: 'Delete';
}
| {
messageErasureType: 'Unlink';
replaceByUser: { _id: IUser['_id']; username: IUser['username']; alias: string };
},
): void;
'user.deleteCustomStatus'(userStatus: Omit<ICustomUserStatus, '_updatedAt'>): void;
'user.forceLogout': (uid: string, sessionId?: string) => void;
'user.media-signal'(data: { userId: IUser['_id']; signal: ServerMediaSignal }): void;
'user.nameChanged'(user: Pick<IUser, '_id' | 'name' | 'username'>): void;
'user.realNameChanged'(user: Partial<IUser>): void;
'user.roleUpdate'(update: {
type: 'added' | 'removed' | 'changed';
_id: string;
u?: { _id: IUser['_id']; username: IUser['username']; name?: IUser['name'] };
scope?: string;
}): void;
'user.updateCustomStatus'(userStatus: Omit<ICustomUserStatus, '_updatedAt'>): void;
'user.activity'(data: { user: string; isTyping: boolean; roomId: string }): void;
'user.video-conference'(data: {
userId: IUser['_id'];
action: string;
params: {
callId: VideoConference['_id'];
uid: IUser['_id'];
rid: IRoom['_id'];
};
}): void;
'presence.invalidateVisibility'(data: { targets?: IUser['_id'][]; viewers?: IUser['_id'][] }): void;
'presence.status'(data: {
user: Pick<IUser, '_id' | 'username' | 'status' | 'statusText' | 'statusSource' | 'statusExpiresAt' | 'name' | 'roles'>;
previousStatus: UserStatus | undefined;
}): void;
'watch.messages'(data: { message: IMessage }): void;
'watch.roles'(
data:
| { clientAction: Exclude<ClientAction, 'removed'>; role: IRole }
| {
clientAction: 'removed';
role: {
_id: string;
name: string;
};
},
): void;
'watch.rooms'(data: { clientAction: ClientAction; room: Pick<IRoom, '_id'> | IRoom }): void;
'watch.subscriptions'(
data:
| {
clientAction: 'updated' | 'inserted';
subscription: Pick<
ISubscription,
| 't'
| 'ts'
| 'ls'
| 'lr'
| 'name'
| 'fname'
| 'rid'
| 'code'
| 'f'
| 'u'
| 'open'
| 'alert'
| 'roles'
| 'unread'
| 'prid'
| 'userMentions'
| 'groupMentions'
| 'archived'
| 'audioNotificationValue'
| 'desktopNotifications'
| 'mobilePushNotifications'
| 'emailNotifications'
| 'desktopPrefOrigin'
| 'mobilePrefOrigin'
| 'emailPrefOrigin'
| 'unreadAlert'
| '_updatedAt'
| 'blocked'
| 'blocker'
| 'autoTranslate'
| 'autoTranslateLanguage'
| 'disableNotifications'
| 'hideUnreadStatus'
| 'hideMentionStatus'
| 'muteGroupMentions'
| 'ignored'
| 'E2EKey'
| 'E2ESuggestedKey'
| 'oldRoomKeys'
| 'tunread'
| 'tunreadGroup'
| 'tunreadUser'
// Omnichannel fields
| 'department'
| 'v'
| 'onHold'
>;
}
| {
clientAction: 'removed';
subscription: {
_id: string;
u?: Pick<IUser, '_id' | 'username' | 'name'>;
rid?: string;
t?: string;
};
},
): void;
'watch.inquiries'(data: { clientAction: ClientAction; inquiry: ILivechatInquiryRecord; diff?: undefined | Record<string, any> }): void;
'watch.settings'(data: { clientAction: ClientAction; setting: ISetting }): void;
'watch.users'(
data: {
id: string;
} & (
| {
clientAction: 'inserted';
data: IUser;
}
| {
clientAction: 'removed';
}
| {
clientAction: 'updated';
diff: Record<string, any>;
unset: Record<string, number>;
}
),
): void;
'watch.loginServiceConfiguration'(data: LoginServiceConfigurationEvent): void;
'watch.instanceStatus'(data: {
clientAction: ClientAction;
data?: undefined | Partial<IInstanceStatus>;
diff?: undefined | Record<string, any>;
id: string;
}): void;
'watch.integrationHistory'(data: {
clientAction: ClientAction;
data: Partial<IIntegrationHistory>;
diff?: undefined | Record<string, any>;
id: string;
}): void;
'watch.integrations'(data: { clientAction: ClientAction; data: Partial<IIntegration>; id: string }): void;
'watch.emailInbox'(data: { clientAction: ClientAction; data: Partial<IEmailInbox>; id: string }): void;
'watch.livechatDepartmentAgents'(data: {
clientAction: ClientAction;
data: Partial<ILivechatDepartmentAgents>;
diff?: undefined | Record<string, any>;
id: string;
}): void;
'omnichannel.room'(
roomId: string,
data:
| { type: 'agentStatus'; status: string }
| { type: 'queueData'; data: { [k: string]: unknown } | undefined }
| { type: 'agentData'; data: ILivechatAgent | undefined | { hiddenInfo: boolean } }
| { type: 'visitorData'; visitor: ILivechatVisitor },
): void;
// Send all events from here
'call.callerhangup'(userId: string, data: { roomId: string }): void;
'connector.statuschanged'(enabled: boolean): void;
'federation.userRoleChanged'(update: Record<string, any>): void;
'watch.priorities'(data: { clientAction: ClientAction; id: ILivechatPriority['_id']; diff?: Record<string, string> }): void;
'apps.added'(appId: string): void;
'apps.removed'(appId: string): void;
'apps.updated'(appId: string, originInstanceId?: string): void;
'apps.statusUpdate'(appId: string, status: AppStatus): void;
'apps.settingUpdated'(appId: string, setting: AppsSetting): void;
'command.added'(command: string): void;
'command.disabled'(command: string): void;
'command.updated'(command: string): void;
'command.removed'(command: string): void;
'actions.changed'(): void;
'media-call.updated'(data: { callId: string; dtmf?: ClientMediaSignalBody<'dtmf'> }): void;
};

View File

@@ -0,0 +1,227 @@
import { proxify } from './lib/proxify';
import type {
IAISearchService,
AISearchAnswerMessage,
AISearchAnswerResult,
AISearchFilters,
AISearchModelOption,
AISearchResult,
AISearchStatus,
} from './types/IAISearchService';
import type { IAbacService } from './types/IAbacService';
import type { IAccount, ILoginResult } from './types/IAccount';
import type { IAnalyticsService } from './types/IAnalyticsService';
import type { IApiService } from './types/IApiService';
import type { IAppsEngineService } from './types/IAppsEngineService';
import type { IAuthorization, RoomAccessValidator, UserWithRoles } from './types/IAuthorization';
import type { IAuthorizationLivechat } from './types/IAuthorizationLivechat';
import type { IBannerService } from './types/IBannerService';
import type { ICalendarService } from './types/ICalendarService';
import type { ICallHistoryService } from './types/ICallHistoryService';
import type { IDeviceManagementService } from './types/IDeviceManagementService';
import type { IEnterpriseSettings } from './types/IEnterpriseSettings';
import type { IFederationMatrixService } from './types/IFederationMatrixService';
import type { IFederationService, IFederationServiceEE } from './types/IFederationService';
import type { IImportService } from './types/IImportService';
import type { ILDAPEEService } from './types/ILDAPEEService';
import type { ILDAPService } from './types/ILDAPService';
import type { ILicense } from './types/ILicense';
import type { IMediaCallService } from './types/IMediaCallService';
import type { IMediaService, ResizeResult } from './types/IMediaService';
import type { IMessageReadsService } from './types/IMessageReadsService';
import type { IMessageService } from './types/IMessageService';
import type { IMeteor, AutoUpdateRecord } from './types/IMeteor';
import type { INPSService, NPSCreatePayload, NPSVotePayload } from './types/INPSService';
import type { IOmnichannelAnalyticsService } from './types/IOmnichannelAnalyticsService';
import type { IOmnichannelEEService } from './types/IOmnichannelEEService';
import type { IOmnichannelIntegrationService } from './types/IOmnichannelIntegrationService';
import type { IOmnichannelService } from './types/IOmnichannelService';
import type { IOmnichannelTranscriptService } from './types/IOmnichannelTranscriptService';
import type { IPresence } from './types/IPresence';
import type { IPushService } from './types/IPushService';
import type { IQueueWorkerService, HealthAggResult } from './types/IQueueWorkerService';
import type { IRoomService, ICreateRoomParams, ISubscriptionExtraData } from './types/IRoomService';
import type { ISAUMonitorService } from './types/ISAUMonitorService';
import type { ISettingsService } from './types/ISettingsService';
import type { IStatusVisibilityService } from './types/IStatusVisibilityService';
import type {
ITeamService,
ITeamUpdateData,
ITeamMemberParams,
ITeamMemberInfo,
ITeamInfo,
ITeamCreateParams,
ITeamAutocompleteResult,
IListRoomsFilter,
} from './types/ITeamService';
import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent';
import type {
UiKitCoreAppBlockActionPayload,
UiKitCoreAppViewClosedPayload,
UiKitCoreAppViewSubmitPayload,
IUiKitCoreApp,
IUiKitCoreAppService,
} from './types/IUiKitCoreApp';
import type { ISendFileLivechatMessageParams, ISendFileMessageParams, IUploadFileParams, IUploadService } from './types/IUploadService';
import type { IUserService } from './types/IUserService';
import type { IVideoConfService, VideoConferenceJoinOptions } from './types/IVideoConfService';
export type { AppStatusReport } from './types/IAppsEngineService';
export type { IAbacService, AbacActor } from './types/IAbacService';
export { asyncLocalStorage } from './lib/asyncLocalStorage';
export { MeteorError, isMeteorError } from './MeteorError';
export { api } from './api';
export type { EventSignatures } from './events/Events';
export { LocalBroker } from './LocalBroker';
export type { IBroker, IBrokerNode, BaseMetricOptions, CallingOptions, IServiceMetrics } from './types/IBroker';
export type { IServiceContext, IServiceClass } from './types/ServiceClass';
export { ServiceClass, ServiceClassInternal } from './types/ServiceClass';
export type {
IFederationService,
IFederationServiceEE,
IFederationJoinExternalPublicRoomInput,
FederationConfigurationStatus,
} from './types/IFederationService';
export type { IFederationMatrixService } from './types/IFederationMatrixService';
export type {
ConversationData,
AgentOverviewDataOptions,
ChartDataOptions,
AnalyticsOverviewDataOptions,
ChartDataResult,
AnalyticsOverviewDataResult,
} from './types/IOmnichannelAnalyticsService';
export { getConnection, getTrashCollection } from './lib/mongo';
export { ServiceStarter } from './lib/ServiceStarter';
export type { ICreateRoomOptions } from './types/IRoomService';
export type {
AutoUpdateRecord,
IAccount,
IAnalyticsService,
IApiService,
IAppsEngineService,
IAuthorization,
IAuthorizationLivechat,
IBannerService,
ICreateRoomParams,
IDeviceManagementService,
IEnterpriseSettings,
ILDAPEEService,
ILDAPService,
ILicense,
IListRoomsFilter,
ILoginResult,
IMediaService,
IMeteor,
INPSService,
IOmnichannelService,
IPresence,
IPushService,
IMediaCallService,
IMessageReadsService,
IRoomService,
ISAUMonitorService,
ISubscriptionExtraData,
ITeamAutocompleteResult,
ITeamCreateParams,
ITeamInfo,
ITeamMemberInfo,
ITeamMemberParams,
ITeamService,
ITeamUpdateData,
ITelemetryEvent,
UiKitCoreAppBlockActionPayload,
UiKitCoreAppViewClosedPayload,
UiKitCoreAppViewSubmitPayload,
IUiKitCoreApp,
IUiKitCoreAppService,
IVideoConfService,
NPSCreatePayload,
NPSVotePayload,
ResizeResult,
RoomAccessValidator,
UserWithRoles,
TelemetryEvents,
TelemetryMap,
VideoConferenceJoinOptions,
ISendFileLivechatMessageParams,
ISendFileMessageParams,
IUploadFileParams,
IUploadService,
ICalendarService,
IAISearchService,
AISearchAnswerMessage,
AISearchAnswerResult,
AISearchFilters,
AISearchModelOption,
AISearchResult,
AISearchStatus,
ICallHistoryService,
IOmnichannelTranscriptService,
IQueueWorkerService,
HealthAggResult,
IMessageService,
ISettingsService,
IStatusVisibilityService,
IOmnichannelEEService,
IOmnichannelIntegrationService,
IImportService,
IOmnichannelAnalyticsService,
IUserService,
};
export { proxify };
// TODO think in a way to not have to pass the service name to proxify here as well
export const Authorization = proxify<IAuthorization>('authorization');
export const Apps = proxify<IAppsEngineService>('apps-engine');
export const Presence = proxify<IPresence>('presence');
export const Account = proxify<IAccount>('accounts');
export const License = proxify<ILicense>('license');
export const MeteorService = proxify<IMeteor>('meteor');
export const Banner = proxify<IBannerService>('banner');
export const UiKitCoreApp = proxify<IUiKitCoreAppService>('uikit-core-app');
export const NPS = proxify<INPSService>('nps');
export const Team = proxify<ITeamService>('team');
export const MessageReads = proxify<IMessageReadsService>('message-reads');
export const Room = proxify<IRoomService>('room');
export const Media = proxify<IMediaService>('media');
export const MediaCall = proxify<IMediaCallService>('media-call');
export const Analytics = proxify<IAnalyticsService>('analytics');
export const LDAP = proxify<ILDAPService>('ldap');
export const LDAPEnterprise = proxify<ILDAPEEService>('ldap-enterprise');
export const SAUMonitor = proxify<ISAUMonitorService>('sau-monitor');
export const DeviceManagement = proxify<IDeviceManagementService>('device-management');
export const VideoConf = proxify<IVideoConfService>('video-conference');
export const Upload = proxify<IUploadService>('upload');
export const Calendar = proxify<ICalendarService>('calendar');
export const CallHistory = proxify<ICallHistoryService>('call-history');
export const QueueWorker = proxify<IQueueWorkerService>('queue-worker');
export const OmnichannelTranscript = proxify<IOmnichannelTranscriptService>('omnichannel-transcript');
export const Message = proxify<IMessageService>('message');
export const Settings = proxify<ISettingsService>('settings');
export const StatusVisibility = proxify<IStatusVisibilityService>('status-visibility');
export const OmnichannelIntegration = proxify<IOmnichannelIntegrationService>('omnichannel-integration');
export const Federation = proxify<IFederationService>('federation');
export const FederationEE = proxify<IFederationServiceEE>('federation-enterprise');
export const Omnichannel = proxify<IOmnichannelService>('omnichannel');
export const OmnichannelEEService = proxify<IOmnichannelEEService>('omnichannel-ee');
export const Import = proxify<IImportService>('import');
export const OmnichannelAnalytics = proxify<IOmnichannelAnalyticsService>('omnichannel-analytics');
export const User = proxify<IUserService>('user');
export const Push = proxify<IPushService>('push');
// Calls without wait. Means that the service is optional and the result may be an error
// of service/method not available
export const EnterpriseSettings = proxify<IEnterpriseSettings>('ee-settings');
export const FederationMatrix = proxify<IFederationMatrixService>('federation-matrix');
export const Abac = proxify<IAbacService>('abac');
export const AISearch = proxify<IAISearchService>('ai-search');

View File

@@ -0,0 +1,77 @@
import type { EventSignatures } from '../events/Events';
import type { IApiService } from '../types/IApiService';
import type { CallingOptions, IBroker, IBrokerNode } from '../types/IBroker';
import type { IServiceClass } from '../types/ServiceClass';
export class Api implements IApiService {
private services: Set<IServiceClass> = new Set<IServiceClass>();
private broker?: IBroker;
// set a broker for the API and registers all services in the broker
setBroker(broker: IBroker): void {
this.broker = broker;
this.services.forEach((service) => this.broker?.createService(service));
}
async destroyService(instance: IServiceClass): Promise<void> {
if (!this.services.has(instance)) {
return;
}
if (this.broker) {
await this.broker.destroyService(instance);
}
this.services.delete(instance);
}
registerService(instance: IServiceClass, serviceDependencies?: string[]): void {
this.services.add(instance);
instance.setApi(this);
if (this.broker) {
this.broker.createService(instance, serviceDependencies);
}
}
async call(method: string, data?: unknown, options?: CallingOptions): Promise<any> {
return this.broker?.call(method, data, options);
}
async broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
if (!this.broker) {
throw new Error(`No broker set to broadcast: ${event}, ${JSON.stringify(args)}`);
}
return this.broker.broadcast(event, ...args);
}
async broadcastToServices<T extends keyof EventSignatures>(
services: string[],
event: T,
...args: Parameters<EventSignatures[T]>
): Promise<void> {
return this.broker?.broadcastToServices(services, event, ...args);
}
async broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void> {
return this.broker?.broadcastLocal(event, ...args);
}
nodeList(): Promise<IBrokerNode[]> {
if (!this.broker) {
throw new Error('No broker set to start.');
}
return this.broker.nodeList();
}
async start(): Promise<void> {
if (!this.broker) {
throw new Error('No broker set to start.');
}
await this.broker.start();
}
}

View File

@@ -0,0 +1,9 @@
import { AsyncLocalStorage } from 'async_hooks';
interface IContextStore<T> {
getStore(): T | undefined;
run(store: T, callback: (...args: any) => void, ...args: any): void;
}
// This is the default implementation of the context store but there is a bug on Meteor 2.5 that prevents us from using it
export class AsyncContextStore<T> extends AsyncLocalStorage<T> implements IContextStore<T> {}

View File

@@ -0,0 +1,68 @@
// This class is used to manage calls to a service's .start and .stop functions
// Specifically for cases where the start function has different conditions that may cause the service to actually start or not,
// or when the start process can take a while to complete
// Using this class, you ensure that calls to .start and .stop will be chained, so you avoid race conditions
// At the same time, it prevents those functions from running more times than necessary if there are several calls to them (for example when loading setting values)
export class ServiceStarter {
private lock = Promise.resolve();
private currentCall?: 'start' | 'stop';
private nextCall?: 'start' | 'stop';
private starterFn: () => Promise<void>;
private stopperFn?: () => Promise<void>;
constructor(starterFn: () => Promise<void>, stopperFn?: () => Promise<void>) {
this.starterFn = starterFn;
this.stopperFn = stopperFn;
}
private async checkStatus(): Promise<void> {
if (this.nextCall === 'start') {
return this.doCall('start');
}
if (this.nextCall === 'stop') {
return this.doCall('stop');
}
}
private async doCall(call: 'start' | 'stop'): Promise<void> {
this.nextCall = undefined;
this.currentCall = call;
try {
if (call === 'start') {
await this.starterFn();
} else if (this.stopperFn) {
await this.stopperFn();
}
} finally {
this.currentCall = undefined;
await this.checkStatus();
}
}
private async call(call: 'start' | 'stop'): Promise<void> {
// If something is already chained to run after the current call, it's okay to replace it with the new call
this.nextCall = call;
if (this.currentCall) {
return this.lock;
}
this.lock = this.checkStatus();
return this.lock;
}
async start(): Promise<void> {
return this.call('start');
}
async stop(): Promise<void> {
return this.call('stop');
}
async wait(): Promise<void> {
return this.lock;
}
}

View File

@@ -0,0 +1,4 @@
import { AsyncContextStore } from './ContextStore';
import type { IServiceContext } from '../types/ServiceClass';
export const asyncLocalStorage = new AsyncContextStore<IServiceContext>();

View File

@@ -0,0 +1,47 @@
import { isTracingEnabled } from '@rocket.chat/tracing';
import { MongoClient } from 'mongodb';
import type { Db, Collection, MongoClientOptions, Document } from 'mongodb';
const { MONGO_URL = 'mongodb://localhost:27017/rocketchat' } = process.env;
const name = /^mongodb:\/\/.*?(?::[0-9]+)?\/([^?]*)/.exec(MONGO_URL)?.[1];
function connectDb(options?: MongoClientOptions): Promise<MongoClient> {
const client = new MongoClient(MONGO_URL, {
...options,
monitorCommands: isTracingEnabled(),
});
return client.connect().catch((error) => {
// exits the process in case of any error
console.error(error);
process.exit(1);
});
}
let db: Db;
export const getConnection = ((): ((options?: MongoClientOptions) => Promise<{ db: Db; client: MongoClient }>) => {
let client: MongoClient;
return async (options): Promise<{ db: Db; client: MongoClient }> => {
if (db) {
return { db, client };
}
if (client == null) {
client = await connectDb(options);
db = client.db(name);
}
// if getConnection was called multiple times before it was connected, wait for the connection
return { client, db: client.db(name) };
};
})();
export async function getTrashCollection<T extends Document>(): Promise<Collection<T>> {
if (!db) {
const { db: clientDb } = await getConnection();
db = clientDb;
}
return db.collection<T>('rocketchat__trash');
}

View File

@@ -0,0 +1,22 @@
import { api } from '../api';
type Promisify<T> = {
[K in keyof T as T[K] extends (...params: any[]) => unknown ? K : never]: T[K] extends (...params: any[]) => Promise<any>
? T[K]
: T[K] extends (...params: infer P) => infer R
? (...params: P) => Promise<R>
: never;
};
function handler<T extends object>(namespace: string): ProxyHandler<T> {
return {
get:
(_target: T, prop: string): any =>
(...params: any): Promise<any> =>
api.call(`${namespace}.${prop}`, params),
};
}
export function proxify<T>(namespace: string): Promisify<T> {
return new Proxy({}, handler(namespace)) as unknown as Promisify<T>;
}

View File

@@ -0,0 +1,62 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export type AISearchFilters = {
rid?: string;
rids?: string[];
roomNames?: string[];
fromUsername?: string;
fromUsernames?: string[];
startDate?: string | Date;
endDate?: string | Date;
};
export type AISearchStatus = {
hasIntelligentSearchLicense: boolean;
intelligentSearchEnabled: boolean;
intelligentSearchConfigured: boolean;
answerGenerationConfigured: boolean;
};
export type AISearchAnswerMessage = {
text: string;
username?: string;
roomName?: string;
ts?: string;
score?: number;
};
export type AISearchAnswerResult = {
answer: string;
provider: {
name: string;
model: string;
};
};
export type AISearchModelOption = {
key: string;
label: string;
};
export type AISearchResult = {
_id: string;
rid?: string;
msgId?: string;
text: string;
score?: number;
ts?: string;
u?: { username: string; name?: string };
room?: Pick<IRoom, '_id' | 't' | 'name' | 'fname'>;
};
export interface IAISearchService extends IServiceClass {
status(): Promise<AISearchStatus>;
search(params: { query: string; userId: string; filters?: AISearchFilters; limit?: number }): Promise<AISearchResult[]>;
answer(params: { query: string; messages: AISearchAnswerMessage[] }): Promise<AISearchAnswerResult>;
models(): Promise<AISearchModelOption[]>;
}

View File

@@ -0,0 +1,56 @@
import type {
IAbacAttributeDefinition,
IAbacAttribute,
IRoom,
IRoomAbacRedaction,
IUser,
AbacAccessOperation,
AbacObjectType,
ILDAPEntry,
AbacUserIdentifiers,
} from '@rocket.chat/core-typings';
export type AbacActor = Pick<IUser, '_id' | 'username' | 'name'>;
export interface IAbacService {
addAbacAttribute(attribute: IAbacAttributeDefinition, actor: AbacActor | undefined): Promise<void>;
listAbacAttributes(
filters?: {
key?: string;
values?: string;
offset?: number;
count?: number;
},
actor?: AbacActor,
): Promise<{ attributes: Pick<IAbacAttribute, '_id' | 'key' | 'values'>[]; offset: number; count: number; total: number }>;
listAbacRooms(
filters?: {
offset?: number;
count?: number;
filter?: string;
filterType?: 'all' | 'roomName' | 'attribute' | 'value';
},
actor?: AbacActor,
): Promise<{ rooms: Array<IRoom & IRoomAbacRedaction>; offset: number; count: number; total: number }>;
scopeRoomsForAdmin<T extends Pick<IRoom, '_id' | 'abacAttributes'>>(rooms: T[], actor: AbacActor): Promise<Array<T & IRoomAbacRedaction>>;
updateAbacAttributeById(_id: string, update: { key?: string; values?: string[] }, actor: AbacActor | undefined): Promise<void>;
deleteAbacAttributeById(_id: string, actor: AbacActor | undefined): Promise<void>;
getAbacAttributeById(_id: string, actor: AbacActor | undefined): Promise<{ key: string; values: string[] }>;
isAbacAttributeInUseByKey(key: string): Promise<boolean>;
setRoomAbacAttributes(rid: string, attributes: Record<string, string[]>, actor: AbacActor | undefined): Promise<void>;
removeRoomAbacAttribute(rid: string, key: string, actor: AbacActor | undefined): Promise<void>;
addRoomAbacAttributeByKey(rid: string, key: string, values: string[], actor: AbacActor | undefined): Promise<void>;
replaceRoomAbacAttributeByKey(rid: string, key: string, values: string[], actor: AbacActor | undefined): Promise<void>;
checkUsernamesMatchAttributes(usernames: string[], attributes: IAbacAttributeDefinition[], object: IRoom): Promise<void>;
canAccessObject(
room: Pick<IRoom, '_id' | 't' | 'teamId' | 'prid' | 'abacAttributes'>,
user: Pick<IUser, '_id'>,
action: AbacAccessOperation,
objectType: AbacObjectType,
): Promise<boolean>;
addSubjectAttributes(user: IUser, ldapUser: ILDAPEntry, map: Record<string, string>, actor: AbacActor | undefined): Promise<void>;
evaluateRoomMembership(): Promise<void>;
reevaluateUsers(identifiers: AbacUserIdentifiers): Promise<void>;
getPDPHealth(): Promise<void>;
isExternalAttributeStore(): Promise<boolean>;
}

View File

@@ -0,0 +1,14 @@
import type { IServiceClass } from './ServiceClass';
export interface ILoginResult {
uid: string;
token: string;
hashedToken: string;
tokenExpires?: Date;
type: 'resume';
}
export interface IAccount extends IServiceClass {
login({ resume }: { resume: string }): Promise<false | ILoginResult>;
logout({ userId, token }: { userId: string; token: string }): Promise<void>;
}

View File

@@ -0,0 +1,7 @@
import type { IServiceClass } from './ServiceClass';
export interface IAnalyticsService extends IServiceClass {
saveSeatRequest(): Promise<void>;
getSeatRequestCount(): Promise<number>;
resetSeatRequestCount(): Promise<void>;
}

View File

@@ -0,0 +1,25 @@
import type { CallingOptions, IBroker, IBrokerNode } from './IBroker';
import type { IServiceClass } from './ServiceClass';
import type { EventSignatures } from '../events/Events';
export interface IApiService {
setBroker(broker: IBroker): void;
destroyService(instance: IServiceClass): Promise<void>;
registerService(instance: IServiceClass): void;
call(method: string, data?: unknown, options?: CallingOptions): Promise<any>;
broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
broadcastToServices<T extends keyof EventSignatures>(
services: string[],
event: T,
...args: Parameters<EventSignatures[T]>
): Promise<void>;
broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
nodeList(): Promise<IBrokerNode[]>;
}

View File

@@ -0,0 +1,16 @@
import type { IGetAppsFilter } from '@rocket.chat/apps/dist/server/IGetAppsFilter';
import type { IAppStorageItem } from '@rocket.chat/apps/dist/server/storage';
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
export type AppStatusReport = {
[appId: string]: { instanceId: string; isLocal: boolean; status: AppStatus }[];
};
export interface IAppsEngineService {
isInitialized(): boolean;
getApps(query: IGetAppsFilter): Promise<IAppInfo[] | undefined>;
getAppStorageItemById(appId: string): Promise<IAppStorageItem | undefined>;
getAppsStatusLocal(): Promise<{ appId: string; status: AppStatus }[]>;
getAppsStatusInNodes(): Promise<AppStatusReport>;
}

View File

@@ -0,0 +1,22 @@
import type { IRoom, IUser, IRole } from '@rocket.chat/core-typings';
/** Minimal user shape a permission check needs. `getRoles` only reads `_id` and `roles`. */
export type UserWithRoles = Pick<IUser, '_id' | 'roles'>;
export type RoomAccessValidator = (
room?: Pick<IRoom, '_id' | 't' | 'teamId' | 'prid' | 'abacAttributes'>,
user?: IUser | Pick<IUser, '_id'>,
extraData?: Record<string, any>,
) => Promise<boolean>;
export interface IAuthorization {
hasAllPermission(user: string | UserWithRoles, permissions: string[], scope?: string): Promise<boolean>;
hasPermission(user: string | UserWithRoles, permissionId: string, scope?: string): Promise<boolean>;
hasAtLeastOnePermission(user: string | UserWithRoles, permissions: string[], scope?: string): Promise<boolean>;
canAccessRoom: RoomAccessValidator;
canReadRoom: RoomAccessValidator;
canAccessRoomId(rid: IRoom['_id'], uid?: IUser['_id']): Promise<boolean>;
canAccessRoomIds(rids: IRoom['_id'][], user: UserWithRoles): Promise<boolean>;
getUsersFromPublicRoles(): Promise<Pick<Required<IUser>, '_id' | 'username' | 'roles'>[]>;
hasAnyRole(userId: IUser['_id'], roleIds: IRole['_id'][], scope?: IRoom['_id']): Promise<boolean>;
}

View File

@@ -0,0 +1,5 @@
import type { IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
export interface IAuthorizationLivechat {
canAccessRoom: (room: IOmnichannelRoom, user?: Pick<IUser, '_id'>, extraData?: Record<string, any>) => Promise<boolean>;
}

View File

@@ -0,0 +1,11 @@
import type { BannerPlatform, IBanner, Optional } from '@rocket.chat/core-typings';
export interface IBannerService {
getBannersForUser(userId: string, platform: BannerPlatform, bannerId?: string): Promise<IBanner[]>;
create(banner: Optional<IBanner, '_id' | '_updatedAt'>): Promise<IBanner>;
dismiss(userId: string, bannerId: string): Promise<boolean>;
discardDismissal(bannerId: string): Promise<boolean>;
getById(bannerId: string): Promise<null | IBanner>;
disable(bannerId: string): Promise<boolean>;
enable(bannerId: string, doc?: Partial<Omit<IBanner, '_id'>>): Promise<boolean>;
}

View File

@@ -0,0 +1,77 @@
import type { IServiceClass } from './ServiceClass';
import type { EventSignatures } from '../events/Events';
export interface IBrokerNode {
id: string;
instanceID?: string;
available: boolean;
local?: boolean;
// lastHeartbeatTime: 16,
// config: {},
// client: { type: 'nodejs', version: '0.14.10', langVersion: 'v12.18.3' },
// metadata: {},
// ipList: [ '192.168.0.100', '192.168.1.25' ],
// port: 59989,
// hostname: 'service.local-1',
// udpAddress: null,
// cpu: 25,
// cpuSeq: 1,
// seq: 3,
// offlineSince: null
}
export type BaseMetricOptions = {
type: string;
name: string;
description?: string;
labelNames?: Array<string>;
unit?: string;
aggregator?: string;
[key: string]: unknown;
};
export type CallingOptions = {
nodeID?: string;
// timeout?: number;
// retries?: number;
// fallbackResponse?: FallbackResponse | FallbackResponse[] | FallbackResponseHandler;
// meta?: GenericObject;
// parentSpan?: ContextParentSpan;
// parentCtx?: Context;
// requestID?: string;
// tracking?: boolean;
// paramsCloning?: boolean;
// caller?: string;
};
export interface IServiceMetrics {
register(opts: BaseMetricOptions): void;
hasMetric(name: string): boolean;
increment(name: string, labels?: Record<string, any>, value?: number, timestamp?: number): void;
decrement(name: string, labels?: Record<string, any>, value?: number, timestamp?: number): void;
set(name: string, value: any | null, labels?: Record<string, any>, timestamp?: number): void;
observe(name: string, value: number, labels?: Record<string, any>, timestamp?: number): void;
reset(name: string, labels?: Record<string, any>, timestamp?: number): void;
resetAll(name: string, timestamp?: number): void;
timer(name: string, labels?: Record<string, any>, timestamp?: number): () => number;
}
export interface IBroker {
metrics?: IServiceMetrics;
destroyService(service: IServiceClass): Promise<void>;
createService(service: IServiceClass, serviceDependencies?: string[]): void;
call(method: string, data: any, options?: CallingOptions): Promise<any>;
broadcastToServices<T extends keyof EventSignatures>(
services: string[],
event: T,
...args: Parameters<EventSignatures[T]>
): Promise<void>;
broadcast<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
broadcastLocal<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): Promise<void>;
nodeList(): Promise<IBrokerNode[]>;
start(): Promise<void>;
}

View File

@@ -0,0 +1,14 @@
import type { ICalendarEvent, IUser } from '@rocket.chat/core-typings';
import type { InsertionModel } from '@rocket.chat/model-typings';
import type { UpdateResult, DeleteResult } from 'mongodb';
export interface ICalendarService {
create(data: Omit<InsertionModel<ICalendarEvent>, 'reminderTime' | 'notificationSent'>): Promise<ICalendarEvent['_id']>;
import(data: Omit<InsertionModel<ICalendarEvent>, 'notificationSent'>): Promise<ICalendarEvent['_id']>;
get(eventId: ICalendarEvent['_id']): Promise<ICalendarEvent | null>;
list(uid: IUser['_id'], date: Date): Promise<ICalendarEvent[]>;
update(eventId: ICalendarEvent['_id'], data: Partial<ICalendarEvent>): Promise<UpdateResult | null>;
delete(eventId: ICalendarEvent['_id']): Promise<DeleteResult>;
setupNextNotification(): Promise<void>;
setupNextStatusChange(): Promise<void>;
}

View File

@@ -0,0 +1,17 @@
import type { CallHistoryItem, IUser } from '@rocket.chat/core-typings';
export interface ICallHistoryService {
search(
uid: IUser['_id'],
filters: {
searchTerm?: string;
direction?: CallHistoryItem['direction'];
inStates?: CallHistoryItem['state'][];
},
pagination: {
count: number;
offset: number;
sort?: Record<string, 1 | -1>;
},
): Promise<{ items: CallHistoryItem[]; total: number }>;
}

View File

@@ -0,0 +1,3 @@
import type { IServiceClass } from './ServiceClass';
export type IDeviceManagementService = IServiceClass;

View File

@@ -0,0 +1,7 @@
import type { ISetting } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export interface IEnterpriseSettings extends IServiceClass {
changeSettingValue(record: ISetting): undefined | ISetting['value'];
}

View File

@@ -0,0 +1,39 @@
import type { IMessage, IRoomFederated, IRoomNativeFederated, ISubscription, IUser } from '@rocket.chat/core-typings';
import type { EventID, EventStore, PduForType } from '@rocket.chat/federation-sdk';
export interface IFederationMatrixService {
createRoom(room: IRoomFederated, owner: IUser): Promise<{ room_id: string; event_id: string }>;
ensureFederatedUsersExistLocally(members: string[]): Promise<void>;
createDirectMessageRoom(room: IRoomFederated, members: IUser[], creatorId: IUser['_id']): Promise<void>;
sendMessage(message: IMessage, room: IRoomFederated, user: IUser): Promise<void>;
deleteMessage(matrixRoomId: string, message: IMessage): Promise<void>;
sendReaction(messageId: string, reaction: string, user: IUser): Promise<void>;
removeReaction(messageId: string, reaction: string, user: IUser, oldMessage: IMessage): Promise<void>;
getEventById(eventId: string): Promise<EventStore | null>;
leaveRoom(rid: IRoomFederated['_id'], user: IUser, kicker?: IUser): Promise<void>;
kickUser(room: IRoomNativeFederated, removedUser: IUser, userWhoRemoved: IUser): Promise<void>;
banUser(room: IRoomNativeFederated, bannedUser: IUser, userWhoBanned: IUser): Promise<void>;
unbanUser(room: IRoomNativeFederated, unbannedUser: IUser, userWhoUnbanned: IUser): Promise<void>;
updateMessage(room: IRoomNativeFederated, message: IMessage): Promise<void>;
updateRoomName(rid: string, displayName: string, user: IUser): Promise<void>;
updateRoomTopic(
room: IRoomNativeFederated,
topic: string,
user: Pick<IUser, '_id' | 'username' | 'federation' | 'federated'>,
): Promise<void>;
addUserRoleRoomScoped(
room: IRoomNativeFederated,
senderId: string,
userId: string,
role: 'moderator' | 'owner' | 'leader' | 'user',
): Promise<void>;
inviteUsersToRoom(room: IRoomFederated, usersUserName: string[], inviter: IUser): Promise<void>;
notifyUserTyping(rid: string, user: string, isTyping: boolean): Promise<void>;
verifyMatrixIds(matrixIds: string[]): Promise<{ [key: string]: string }>;
handleInvite(subscriptionId: ISubscription['_id'], userId: IUser['_id'], action: 'accept' | 'reject'): Promise<void>;
canUserAccessFederation(user: IUser): Promise<boolean>;
notifyRoomRead(params: { room: IRoomNativeFederated; userId: string; threadId?: string }): Promise<void>;
updateUserName(user: IUser): Promise<void>;
joinAppServiceRoom(roomAlias: string, user: IUser): Promise<boolean>;
saveFederationMessage(event: { event: PduForType<'m.room.message'>; event_id: EventID }): Promise<void>;
}

View File

@@ -0,0 +1,70 @@
import type { IRoom } from '@rocket.chat/core-typings';
import type { FederationPaginatedResult, IFederationPublicRooms } from '@rocket.chat/rest-typings';
export type FederationConfigurationStatus = {
appservice: {
error?: string;
ok: boolean;
roundTrip: {
durationMs: number;
};
};
externalReachability: {
error?: string;
ok: boolean;
};
};
interface IFederationBaseService {
verifyMatrixIds(matrixIds: string[]): Promise<Map<string, string>>;
configurationStatus(): Promise<FederationConfigurationStatus>;
markConfigurationValid(): Promise<void>;
markConfigurationInvalid(): Promise<void>;
beforeCreateRoom(room: Partial<IRoom>): Promise<void>;
}
export interface IFederationService extends IFederationBaseService {
createDirectMessageRoomAndInviteUser(internalInviterId: string, internalRoomId: string, externalInviteeId: string): Promise<void>;
deactivateRemoteUser(userId: string): Promise<void>;
}
export interface IFederationJoinExternalPublicRoomInput {
internalUserId: string;
externalRoomId: string;
roomName?: string;
pageToken?: string;
}
export interface IFederationServiceEE extends IFederationBaseService {
createDirectMessageRoom(internalUserId: string, invitees: string[]): Promise<void>;
searchPublicRooms(
serverName?: string,
roomName?: string,
pageToken?: string,
count?: number,
): Promise<
FederationPaginatedResult<{
rooms: IFederationPublicRooms[];
}>
>;
getSearchedServerNamesByInternalUserId(internalUserId: string): Promise<{ name: string; default: boolean; local: boolean }[]>;
addSearchedServerNameByInternalUserId(internalUserId: string, serverName: string): Promise<void>;
removeSearchedServerNameByInternalUserId(internalUserId: string, serverName: string): Promise<void>;
scheduleJoinExternalPublicRoom(internalUserId: string, externalRoomId: string, roomName?: string, pageToken?: string): Promise<void>;
joinExternalPublicRoom(input: IFederationJoinExternalPublicRoomInput): Promise<void>;
verifyMatrixIds(matrixIds: string[]): Promise<Map<string, string>>;
deactivateRemoteUser(userId: string): Promise<void>;
}

View File

@@ -0,0 +1,9 @@
import type { IImport, IImportUser, ImportStatus } from '@rocket.chat/core-typings';
export interface IImportService {
clear(): Promise<void>;
newOperation(userId: string, name: string, key: string): Promise<IImport>;
status(): Promise<ImportStatus>;
addUsers(users: Omit<IImportUser, '_id' | 'services' | 'customFields'>[]): Promise<void>;
run(userId: string): Promise<void>;
}

View File

@@ -0,0 +1,12 @@
import type { IUser } from '@rocket.chat/core-typings';
import type { FindCursor } from 'mongodb';
export interface ILDAPEEService {
sync(): Promise<void>;
syncAvatars(): Promise<void>;
syncAvatarAndAbacAttributes(): Promise<void>;
syncLogout(): Promise<void>;
syncAbacAttributes(): Promise<void>;
syncUsersAbacAttributes(users: FindCursor<IUser>): Promise<void>;
syncUsersAbacAttributesByIds(userIds: string[]): Promise<void>;
}

View File

@@ -0,0 +1,8 @@
import type { LDAPLoginResult } from '@rocket.chat/core-typings';
export interface ILDAPService {
loginRequest(username: string, password: string): Promise<LDAPLoginResult>;
loginAuthenticatedUserRequest(username: string): Promise<LDAPLoginResult>;
testConnection(): Promise<void>;
testSearch(username: string): Promise<void>;
}

View File

@@ -0,0 +1,11 @@
import type { IServiceClass } from './ServiceClass';
export interface ILicense extends IServiceClass {
hasModule(feature: string): boolean;
hasValidLicense(): boolean;
getModules(): string[];
getGuestPermissions(): string[];
}

View File

@@ -0,0 +1,10 @@
import type { IMediaCall, IUser } from '@rocket.chat/core-typings';
import type { ClientMediaSignal, ServerMediaCallSignal, ClientMediaSignalAnswer } from '@rocket.chat/media-signaling';
export interface IMediaCallService {
answerCall(uid: IUser['_id'], params: Omit<ClientMediaSignalAnswer, 'type'>): Promise<IMediaCall>;
processSignal(fromUid: IUser['_id'], signal: ClientMediaSignal): Promise<void>;
processSerializedSignal(fromUid: IUser['_id'], signal: string): Promise<void>;
hangupExpiredCalls(): Promise<void>;
getUserStateSignals(uid: IUser['_id'], contractId: string): Promise<ServerMediaCallSignal[]>;
}

View File

@@ -0,0 +1,33 @@
import type { Readable, Stream } from 'node:stream';
import type { FitEnum } from 'sharp';
export type ResizeResult = {
data: Buffer;
width: number;
height: number;
};
export interface IMediaService {
resizeFromBuffer(
input: Buffer,
width: number,
height: number,
keepType: boolean,
blur: boolean,
enlarge: boolean,
fit?: keyof FitEnum | undefined,
): Promise<ResizeResult>;
resizeFromStream(
input: Readable,
width: number,
height: number,
keepType: boolean,
blur: boolean,
enlarge: boolean,
fit?: keyof FitEnum | undefined,
): Promise<ResizeResult>;
isImage(buff: Buffer): Promise<boolean>;
stripExifFromImageStream(stream: Stream): Readable;
stripExifFromBuffer(buffer: Buffer): Promise<Buffer>;
}

View File

@@ -0,0 +1,5 @@
import type { IServiceClass } from './ServiceClass';
export interface IMessageReadsService extends IServiceClass {
readThread(userId: string, threadId: string): Promise<void>;
}

View File

@@ -0,0 +1,57 @@
import type { IMessage, MessageTypesValues, IUser, IRoom, AtLeast, MessageUrl } from '@rocket.chat/core-typings';
export interface IMessageService {
sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise<IMessage>;
saveSystemMessage<T = IMessage>(
type: MessageTypesValues,
rid: string,
message: string,
user: Pick<IUser, '_id' | 'username' | 'name'>,
extraData?: Partial<T>,
): Promise<IMessage>;
saveMessageFromFederation({
fromId,
rid,
federation_event_id,
msg,
e2e_content,
file,
files,
attachments,
thread,
ts,
}: {
fromId: string;
rid: string;
federation_event_id: string;
msg?: string;
e2e_content?: {
algorithm: string;
ciphertext: string;
};
file?: IMessage['file'];
files?: IMessage['files'];
attachments?: IMessage['attachments'];
thread?: { tmid: string; tshow: boolean };
ts: Date;
}): Promise<IMessage>;
saveSystemMessageAndNotifyUser<T = IMessage>(
type: MessageTypesValues,
rid: string,
message: string,
user: Pick<IUser, '_id' | 'username' | 'name'>,
extraData?: Partial<T>,
): Promise<IMessage>;
beforeSave(param: { message: IMessage; room: IRoom; user: IUser; previewUrls?: string[]; parseUrls?: boolean }): Promise<IMessage>;
sendMessageWithValidation(user: IUser, message: Partial<IMessage>, room: Partial<IRoom>, upsert?: boolean): Promise<IMessage>;
deleteMessage(user: IUser, message: IMessage): Promise<void>;
updateMessage(message: IMessage, user: IUser, originalMsg?: IMessage): Promise<void>;
reactToMessage(userId: string, reaction: string, messageId: IMessage['_id'], shouldReact?: boolean): Promise<void>;
beforeReacted(message: IMessage, room: AtLeast<IRoom, 'federated'>): Promise<void>;
beforeDelete(message: IMessage, room: IRoom): Promise<void>;
afterSave(param: { message: IMessage }): Promise<void>;
parseOEmbedUrl(url: string): Promise<{
urlPreview: MessageUrl;
foundMeta: boolean;
}>;
}

View File

@@ -0,0 +1,31 @@
import type { LoginServiceConfiguration } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export type AutoUpdateRecord = {
_id: string;
version: string;
versionRefreshable?: string;
versionNonRefreshable?: string;
versionHmr: number;
assets?: [
{
url: string;
},
];
};
export interface IMeteor extends IServiceClass {
getAutoUpdateClientVersions(): Promise<Record<string, AutoUpdateRecord>>;
getLoginServiceConfiguration(): Promise<LoginServiceConfiguration[]>;
callMethodWithToken(
userId: string | undefined,
token: string | undefined,
method: string,
args: any[],
): Promise<{
result: unknown;
}>;
notifyGuestStatusChanged(token: string, status: string): Promise<void>;
getURL(path: string, params?: Record<string, any>, cloudDeepLinkUrl?: string): Promise<string>;
getMessageURLToReplyTo(roomType: string, roomId: string, messageIdToReplyTo: string): Promise<string>;
}

View File

@@ -0,0 +1,22 @@
import type { IUser, IRole } from '@rocket.chat/core-typings';
export type NPSVotePayload = {
userId: string | undefined;
npsId: string;
roles?: IRole['_id'][];
score: number;
comment: string;
};
export type NPSCreatePayload = {
npsId: string;
startAt: Date;
expireAt: Date;
createdBy: Pick<IUser, '_id' | 'username'>;
};
export interface INPSService {
create(nps: NPSCreatePayload): Promise<boolean>;
vote(vote: NPSVotePayload): Promise<void>;
sendResults(): Promise<void>;
closeOpenSurveys(): Promise<void>;
}

View File

@@ -0,0 +1,63 @@
import type { IServiceClass } from './ServiceClass';
export type ConversationData = {
head: { name: string }[];
data: { name: string; value: string }[];
};
export type AgentOverviewDataOptions = {
departmentId?: string;
utcOffset?: number;
daterange?: {
from: string;
to: string;
};
chartOptions: {
name: string;
};
executedBy: string;
};
export type ChartDataOptions = {
departmentId?: string;
utcOffset?: number;
daterange?: {
from: string;
to: string;
};
chartOptions: {
name: string;
};
executedBy: string;
};
export type AnalyticsOverviewDataOptions = {
departmentId?: string;
utcOffset?: number;
language: string;
daterange?: {
from: string;
to: string;
};
analyticsOptions: {
name: string;
};
executedBy: string;
};
export type ChartDataResult = {
chartLabel: string;
dataLabels: string[];
dataPoints: number[];
};
export type AnalyticsOverviewDataResult = {
title: string;
value: any;
};
export interface IOmnichannelAnalyticsService extends IServiceClass {
getAgentOverviewData(options: AgentOverviewDataOptions): Promise<ConversationData | void>;
getAnalyticsChartData(options: ChartDataOptions): Promise<ChartDataResult | void>;
getAnalyticsOverviewData(options: AnalyticsOverviewDataOptions): Promise<AnalyticsOverviewDataResult[] | void>;
}

View File

@@ -0,0 +1,18 @@
import type { IOmnichannelRoom, IUser } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export interface IOmnichannelEEService extends IServiceClass {
placeRoomOnHold(
room: Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'u' | 'lastMessage'>,
comment: string,
onHoldBy: Pick<IUser, '_id' | 'username' | 'name'>,
): Promise<void>;
resumeRoomOnHold(
room: Pick<IOmnichannelRoom, '_id' | 't' | 'open' | 'onHold' | 'servedBy' | 'u' | 'lastMessage'>,
comment: string,
resumeBy: Pick<IUser, '_id' | 'username' | 'name'>,
clientAction?: boolean,
): Promise<void>;
}

View File

@@ -0,0 +1,7 @@
import type { ISMSProviderConstructor, ISMSProvider } from '@rocket.chat/core-typings';
export type IOmnichannelIntegrationService = {
getSmsService(name: string): Promise<ISMSProvider>;
registerSmsService(name: string, service: ISMSProviderConstructor): void;
isConfiguredSmsService(name: string): Promise<boolean>;
};

View File

@@ -0,0 +1,7 @@
import type { AtLeast, IOmnichannelRoom } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export interface IOmnichannelService extends IServiceClass {
isWithinMACLimit(_room: AtLeast<IOmnichannelRoom, 'v'>): Promise<boolean>;
}

View File

@@ -0,0 +1,14 @@
import type { IUser, IRoom } from '@rocket.chat/core-typings';
type WorkDetails = {
rid: IRoom['_id'];
userId: IUser['_id'];
};
type WorkDetailsWithSource = WorkDetails & {
from: string;
};
export interface IOmnichannelTranscriptService {
workOnPdf({ details }: { details: WorkDetailsWithSource }): Promise<void>;
}

View File

@@ -0,0 +1,30 @@
import type { IUser, UserStatus } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export interface IPresence extends IServiceClass {
newConnection(
uid: string | undefined,
session: string | undefined,
nodeId: string,
): Promise<{ uid: string; connectionId: string } | undefined>;
removeConnection(
uid: string | undefined,
session: string | undefined,
nodeId: string,
): Promise<{ uid: string; session: string } | undefined>;
updateConnection(uid: string, connectionId: string): Promise<{ uid: string; connectionId: string } | undefined>;
removeLostConnections(nodeID: string): Promise<string[]>;
setStatus(userId: string, status: UserStatus, statusText?: string, statusExpiresAt?: Date): Promise<boolean>;
setActiveState(
userId: string,
newState: Pick<IUser, 'statusDefault' | 'statusSource' | 'statusText' | 'statusExpiresAt' | 'statusId'>,
): Promise<boolean>;
endActiveState(userId: string, statusId?: string): Promise<boolean>;
clearActiveState(userId: string): Promise<boolean>;
setConnectionStatus(uid: string, status: UserStatus, session: string): Promise<boolean>;
toggleBroadcast(enabled: boolean): void;
getConnectionCount(): { current: number; max: number };
getPeakConnections(reset?: boolean): number;
resetPeakConnections(): void;
}

View File

@@ -0,0 +1,9 @@
import type { IPushToken, Optional } from '@rocket.chat/core-typings';
import type { IServiceClass } from './ServiceClass';
export interface IPushService extends IServiceClass {
registerPushToken(
data: Optional<Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata'>, '_id' | 'metadata'>,
): Promise<Omit<IPushToken, 'authToken'>>;
}

View File

@@ -0,0 +1,12 @@
export type HealthAggResult = {
total: number;
type: string;
status: 'Rejected' | 'In progress';
};
type Actions = 'work' | 'workComplete';
export interface IQueueWorkerService {
queueWork<T extends Record<string, unknown>>(queue: Actions, to: string, data: T): Promise<void>;
queueInfo(): Promise<HealthAggResult[]>;
}

View File

@@ -0,0 +1,87 @@
import type { AtLeast, IRoom, ISubscription, IUser, MessageTypesValues } from '@rocket.chat/core-typings';
export interface ISubscriptionExtraData {
open: boolean;
ls?: Date;
prid?: string;
roles?: string[];
}
export interface ICreateRoomOptions extends Partial<Record<string, boolean | string | ISubscriptionExtraData>> {
forceNew?: boolean;
creator: string;
subscriptionExtra?: ISubscriptionExtraData;
}
export interface ICreateRoomParams<T extends IRoom = IRoom> {
type: IRoom['t'];
name: IRoom['name'];
members?: Array<string>; // member's usernames
readOnly?: boolean;
extraData?: Partial<T>;
options?: ICreateRoomOptions;
}
export interface IRoomService {
addMember(uid: string, rid: string): Promise<boolean>;
create<T extends IRoom = IRoom>(uid: string, params: ICreateRoomParams<T>): Promise<IRoom>;
createDirectMessage(data: { to: string; from: string }): Promise<{ rid: string }>;
createDirectMessageWithMultipleUsers(members: string[], creatorId: string): Promise<{ rid: string }>;
addUserToRoom(
roomId: string,
user: Pick<IUser, '_id'> | string,
inviter?: Pick<IUser, '_id' | 'username'>,
options?: {
skipSystemMessage?: boolean;
skipAlertSound?: boolean;
createAsHidden?: boolean;
},
): Promise<boolean | undefined>;
performUserRemoval(room: IRoom, user: IUser, options?: { byUser?: IUser }): Promise<void>;
performUserBan(room: IRoom, user: IUser, byUser: IUser): Promise<void>;
performUserUnban(room: IRoom, user: IUser, byUser: IUser): Promise<void>;
performAcceptRoomInvite(room: IRoom, subscription: ISubscription, user: IUser): Promise<void>;
revokeInvite(room: IRoom, user: IUser): Promise<void>;
removeUserFromRoom(
roomId: string,
user: IUser,
options?: { byUser?: Pick<IUser, '_id' | 'username'>; skipAppPreEvents?: boolean; customSystemMessage?: MessageTypesValues },
): Promise<void>;
getValidRoomName(displayName: string, roomId?: string, options?: { allowDuplicates?: boolean }): Promise<string>;
saveRoomTopic(
roomId: string,
roomTopic: string | undefined,
user: Pick<IUser, 'username' | '_id' | 'federation' | 'federated'>,
sendMessage?: boolean,
): Promise<void>;
getRouteLink(room: AtLeast<IRoom, '_id' | 't' | 'name'>): Promise<string | boolean>;
join(param: { room: IRoom; user: Pick<IUser, '_id' | 'federated' | 'federation'>; joinCode?: string }): Promise<boolean | undefined>;
beforeLeave(room: IRoom): Promise<void>;
beforeUserRemoved(room: IRoom): Promise<void>;
beforeNameChange(room: IRoom): Promise<void>;
beforeTopicChange(room: IRoom): Promise<void>;
saveRoomName(roomId: string, userId: string, name: string): Promise<void>;
addUserRoleRoomScoped(fromUserId: string, userId: string, roomId: string, role: 'moderator' | 'owner' | 'leader' | 'user'): Promise<void>;
createUserSubscription(params: {
room: IRoom;
ts: Date;
userToBeAdded: IUser;
inviter?: Pick<IUser, '_id' | 'username'>;
createAsHidden?: boolean;
skipAlertSound?: boolean;
skipSystemMessage?: boolean;
status?: 'INVITED';
roles?: ISubscription['roles'];
}): Promise<string | undefined>;
updateDirectMessageRoomName(
room: IRoom,
ignoreStatusFromSubs?: string[],
updatedNames?: AtLeast<IUser, '_id' | 'name' | 'username'>[],
): Promise<boolean>;
markAsRead(room: IRoom, userId: string, readThreads?: boolean): Promise<void>;
readThread(params: { user: IUser; room: IRoom; tmid: string }): Promise<void>;
unbanAndInviteUser(
subscription: ISubscription,
inviteeUser: Pick<IUser, '_id' | 'username' | 'name'>,
inviterUser: Required<Pick<IUser, '_id' | 'username'>> & Pick<IUser, 'name'>,
): Promise<void>;
}

View File

@@ -0,0 +1,3 @@
import type { IServiceClass } from './ServiceClass';
export type ISAUMonitorService = IServiceClass;

View File

@@ -0,0 +1,6 @@
import type { SettingValue } from '@rocket.chat/core-typings';
export interface ISettingsService {
get<T extends SettingValue>(settingId: string): Promise<T>;
set<T extends SettingValue>(settingId: string, value: T): Promise<void>;
}

View File

@@ -0,0 +1,9 @@
import type { IUser, UserPresence } from '@rocket.chat/core-typings';
export interface IStatusVisibilityService {
getHiddenFrom(viewerId: IUser['_id'] | null | undefined): Promise<IUser['_id'][]>;
hasRestrictions(targetId: IUser['_id']): Promise<boolean>;
getRestrictedUsers(): Promise<IUser['_id'][]>;
refresh(targets?: IUser['_id'][]): Promise<UserPresence[]>;
invalidate(targets?: IUser['_id'][]): Promise<UserPresence[]>;
}

View File

@@ -0,0 +1,141 @@
import type {
IPaginationOptions,
IQueryOptions,
IRecordsWithTotal,
ITeam,
ITeamMember,
ITeamStats,
TeamType,
IRoom,
IUser,
IRole,
AtLeast,
} from '@rocket.chat/core-typings';
import type { Document, Filter, FindOptions } from 'mongodb';
import type { ICreateRoomParams } from './IRoomService';
export interface ITeamCreateRoom extends Omit<ICreateRoomParams, 'type'> {
id?: string;
}
export interface ITeamCreateParams {
team: Pick<ITeam, 'name' | 'type'>;
room: ITeamCreateRoom;
members?: Array<string> | null; // list of user _ids
owner?: string | null; // the team owner. If not present, owner = requester
}
export interface ITeamMemberParams {
userId: string;
roles?: Array<IRole['_id']> | null;
}
export interface IUserInfo {
_id: string;
username?: string;
name?: string;
status?: string;
settings?: Record<string, any>;
}
export interface ITeamMemberInfo {
user: IUserInfo;
roles?: IRole['_id'][] | null;
createdBy: Omit<IUserInfo, 'name' | 'status'>;
createdAt: Date;
}
export interface ITeamInfo extends ITeam {
rooms: number;
numberOfUsers: number;
}
export interface IListRoomsFilter {
name?: string;
isDefault: boolean;
getAllRooms: boolean;
allowPrivateTeam: boolean;
}
export type ITeamUpdateData = { updateRoom?: boolean } & (
| {
name: string;
type?: TeamType;
}
| {
name?: string;
type: TeamType;
}
);
export type ITeamAutocompleteResult = Pick<IRoom, '_id' | 'fname' | 'teamId' | 'name' | 't' | 'avatarETag'>;
export interface ITeamService {
create(uid: string, params: ITeamCreateParams): Promise<ITeam>;
addRooms(uid: string, rooms: Array<string>, teamId: string): Promise<Array<IRoom>>;
removeRoom(uid: string, rid: string, teamId: string, canRemoveAnyRoom: boolean): Promise<IRoom>;
listRooms(uid: string, teamId: string, filter: IListRoomsFilter, pagination: IPaginationOptions): Promise<IRecordsWithTotal<IRoom>>;
listRoomsOfUser(
uid: string,
teamId: string,
userId: string,
allowPrivateTeam: boolean,
showCanDeleteOnly: boolean,
pagination: IPaginationOptions,
): Promise<IRecordsWithTotal<IRoom>>;
updateRoom(uid: string, rid: string, isDefault: boolean, canUpdateAnyRoom: boolean): Promise<IRoom>;
list(uid: string, paginationOptions?: IPaginationOptions, queryOptions?: IQueryOptions<ITeam>): Promise<IRecordsWithTotal<ITeam>>;
listAll(options?: IPaginationOptions): Promise<IRecordsWithTotal<ITeam>>;
listByNames(names: Array<string>, options?: FindOptions<ITeam>): Promise<Array<ITeam>>;
listByIds(ids: Array<string>, options?: FindOptions<ITeam>): Promise<ITeam[]>;
search(userId: string, term: string | RegExp, options?: FindOptions<ITeam>): Promise<ITeam[]>;
members(
uid: string,
teamId: string,
canSeeAll: boolean,
options?: IPaginationOptions,
queryOptions?: Filter<IUser>,
): Promise<IRecordsWithTotal<ITeamMemberInfo>>;
addMember(inviter: Pick<IUser, '_id' | 'username'>, userId: string, teamId: string): Promise<boolean>;
addMembers(uid: string, teamId: string, members: Array<ITeamMemberParams>): Promise<void>;
updateMember(teamId: string, members: ITeamMemberParams): Promise<void>;
removeMember(teamId: string, userId: string): Promise<void>;
removeMembers(uid: string, teamId: string, members: Array<ITeamMemberParams>): Promise<boolean>;
getInfoByName(teamName: string): Promise<Partial<ITeam> | null>;
getInfoById(teamId: string): Promise<Partial<ITeam> | null>;
deleteById(teamId: string): Promise<boolean>;
deleteByName(teamName: string): Promise<boolean>;
unsetTeamIdOfRooms(user: AtLeast<IUser, '_id' | 'username' | 'name'>, team: AtLeast<ITeam, '_id' | 'roomId'>): Promise<void>;
getOneById(teamId: string, options?: FindOptions<ITeam>): Promise<ITeam | null>;
getOneById<P extends Document>(teamId: string, options?: FindOptions<P extends ITeam ? ITeam : P>): Promise<ITeam | P | null>;
getOneByName(teamName: string | RegExp, options?: FindOptions<ITeam>): Promise<ITeam | null>;
getOneByMainRoomId(teamId: string): Promise<Pick<ITeam, '_id'> | null>;
getOneByRoomId(teamId: string, options?: FindOptions<ITeam>): Promise<ITeam | null>;
getMatchingTeamRooms(teamId: string, rids: Array<string>): Promise<Array<string>>;
autocomplete(uid: string, name: string): Promise<ITeamAutocompleteResult[]>;
getAllPublicTeams(options?: FindOptions<ITeam>): Promise<Array<ITeam>>;
getMembersByTeamIds(teamIds: Array<string>, options: FindOptions<ITeamMember>): Promise<Array<ITeamMember>>;
update(uid: string, teamId: string, updateData: ITeamUpdateData): Promise<void>;
listTeamsBySubscriberUserId(uid: string, options?: FindOptions<ITeamMember>): Promise<Array<ITeamMember> | null>;
insertMemberOnTeams(userId: string, teamIds: Array<string>): Promise<void>;
removeMemberFromTeams(userId: string, teamIds: Array<string>): Promise<void>;
removeAllMembersFromTeam(teamId: string): Promise<void>;
removeRolesFromMember(teamId: string, userId: string, roles: Array<IRole['_id']>): Promise<boolean>;
getStatistics(): Promise<ITeamStats>;
findBySubscribedUserIds(userId: string, callerId?: string): Promise<ITeam[]>;
addRolesToMember(teamId: string, userId: string, roles: Array<string>): Promise<boolean>;
addRolesToSubscription(roomId: string, userId: string, roles: Array<string>): Promise<boolean>;
getRoomInfo(
room: AtLeast<IRoom, 'teamId' | 'teamMain' | '_id'>,
): Promise<{ team?: Pick<ITeam, 'name' | 'roomId' | 'type'>; parentRoom?: Pick<IRoom, 'name' | 'fname' | 't' | '_id'> }>;
listChildren(
userId: string,
team: AtLeast<ITeam, '_id' | 'roomId' | 'type'>,
filter?: string,
type?: 'channels' | 'discussions',
sort?: Record<string, 1 | -1>,
skip?: number,
limit?: number,
): Promise<{ total: number; data: IRoom[] }>;
}

View File

@@ -0,0 +1,11 @@
type updateCounterDataType = { settingsId: string };
type slashCommandsDataType = { command: string };
// TODO this is duplicated from /packages/rest-typings/src/v1/statistics.ts
export type TelemetryMap = { slashCommandsStats: slashCommandsDataType; updateCounter: updateCounterDataType };
export type TelemetryEvents = keyof TelemetryMap;
export interface ITelemetryEvent {
register: (name: TelemetryEvents, fn: () => Promise<any> | void) => void;
call: <T extends TelemetryEvents>(eventName: T, data: TelemetryMap[T]) => Promise<any> | void;
}

View File

@@ -0,0 +1,61 @@
import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import type * as UiKit from '@rocket.chat/ui-kit';
import type { IServiceClass } from './ServiceClass';
export type UiKitCoreAppBlockActionPayload = Pick<
UiKit.ViewBlockActionUserInteraction | UiKit.MessageBlockActionUserInteraction,
'type' | 'actionId' | 'triggerId' | 'container' | 'payload'
> & {
appId: string;
message: IMessage['_id'] | undefined;
user: IUser | undefined;
visitor:
| {
id: string;
username: string;
name?: string;
department?: string;
updatedAt?: Date;
token: string;
phone?: { phoneNumber: string }[] | null;
visitorEmails?: { address: string }[];
livechatData?: Record<string, unknown>;
status?: 'online' | 'away' | 'offline' | 'busy' | 'disabled';
}
| undefined;
room: IRoom['_id'] | undefined;
};
export type UiKitCoreAppViewClosedPayload = Pick<UiKit.ViewClosedUserInteraction, 'type' | 'triggerId'> & {
appId: string;
user: IUser | undefined;
payload: {
view: UiKit.View & {
viewId?: string;
id?: string;
state: { [blockId: string]: { [key: string]: unknown } };
};
isCleared?: boolean;
};
};
export type UiKitCoreAppViewSubmitPayload = Pick<UiKit.ViewSubmitUserInteraction, 'type' | 'actionId' | 'triggerId' | 'payload'> & {
appId: string;
user: IUser | undefined;
};
export interface IUiKitCoreApp {
appId: string;
blockAction?(payload: UiKitCoreAppBlockActionPayload): Promise<UiKit.ServerInteraction | undefined>;
viewClosed?(payload: UiKitCoreAppViewClosedPayload): Promise<UiKit.ServerInteraction | undefined>;
viewSubmit?(payload: UiKitCoreAppViewSubmitPayload): Promise<UiKit.ServerInteraction | undefined>;
}
export interface IUiKitCoreAppService extends IServiceClass {
isRegistered(appId: string): Promise<boolean>;
blockAction(payload: UiKitCoreAppBlockActionPayload): Promise<UiKit.ServerInteraction | undefined>;
viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise<UiKit.ServerInteraction | undefined>;
viewSubmit(payload: UiKitCoreAppViewSubmitPayload): Promise<UiKit.ServerInteraction | undefined>;
}

View File

@@ -0,0 +1,49 @@
import type Stream from 'node:stream';
import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails';
import type { IMessage, IUpload, IUser, FilesAndAttachments, AtLeast } from '@rocket.chat/core-typings';
export interface IUploadFileParams {
userId: string;
buffer: Buffer;
details: IUploadDetails;
federation?: Required<IUpload>['federation'];
}
export interface ISendFileMessageParams {
roomId: string;
userId: string;
file: IUpload;
message?: Partial<IMessage>;
}
export interface ISendFileLivechatMessageParams {
roomId: string;
visitorToken: string;
file: IUpload;
message?: Partial<IMessage>;
}
export interface IUploadService {
uploadFile(params: IUploadFileParams): Promise<IUpload>;
sendFileMessage(params: ISendFileMessageParams): Promise<boolean | undefined>;
sendFileLivechatMessage(params: ISendFileLivechatMessageParams): Promise<boolean | undefined>;
getFileBuffer({ file }: { file: IUpload }): Promise<Buffer>;
extractMetadata(file: IUpload): Promise<{ height?: number; width?: number; format?: string }>;
parseFileIntoMessageAttachments(file: Partial<IUpload>, roomId: string, user: IUser): Promise<FilesAndAttachments>;
canDeleteFile(
user: Pick<IUser, '_id' | 'username'>,
file: Pick<IUpload, '_id' | 'userId' | 'rid' | 'expiresAt' | 'uploadedAt'>,
msg: IMessage | null,
): Promise<boolean>;
deleteFile(user: IUser, fileId: IUpload['_id'], msg: IMessage | null): Promise<{ deletedFiles: IUpload['_id'][] }>;
streamUploadedFile({
file,
imageResizeOpts,
}: {
file: IUpload;
imageResizeOpts?: { width: number; height: number };
}): Promise<Stream.Readable>;
uploadFileFromStream({ streamParam, details }: { streamParam: Stream.Readable; details: Omit<IUploadDetails, 'size'> }): Promise<IUpload>;
setUserAvatar(user: Pick<IUser, '_id' | 'username'>, buffer: Buffer, contentType: string, service: 'rest'): Promise<void>;
resetUserAvatar(user: AtLeast<IUser, '_id' | 'username'>): Promise<void>;
}

View File

@@ -0,0 +1,3 @@
export interface IUserService {
ensureLoginTokensLimit(uid: string): Promise<void>;
}

View File

@@ -0,0 +1,47 @@
import type {
IRoom,
IStats,
IUser,
IVoIPVideoConference,
VideoConference,
VideoConferenceCapabilities,
VideoConferenceCreateData,
VideoConferenceInstructions,
} from '@rocket.chat/core-typings';
import type { InsertionModel } from '@rocket.chat/model-typings';
import type { PaginatedResult } from '@rocket.chat/rest-typings';
import type * as UiKit from '@rocket.chat/ui-kit';
export type VideoConferenceJoinOptions = {
mic?: boolean;
cam?: boolean;
};
export interface IVideoConfService {
create(data: VideoConferenceCreateData, useAppUser?: boolean): Promise<VideoConferenceInstructions>;
start(caller: IUser['_id'], rid: string, options: { title?: string; allowRinging?: boolean }): Promise<VideoConferenceInstructions>;
join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise<string>;
getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise<UiKit.ModalSurfaceLayout>;
cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise<void>;
get(callId: VideoConference['_id']): Promise<Omit<VideoConference, 'providerData'> | null>;
getUnfiltered(callId: VideoConference['_id']): Promise<VideoConference | null>;
list(roomId: IRoom['_id'], pagination?: { offset?: number; count?: number }): Promise<PaginatedResult<{ data: VideoConference[] }>>;
setProviderData(callId: VideoConference['_id'], data: VideoConference['providerData'] | undefined): Promise<void>;
setEndedBy(callId: VideoConference['_id'], endedBy: IUser['_id']): Promise<void>;
setEndedAt(callId: VideoConference['_id'], endedAt: Date): Promise<void>;
setStatus(callId: VideoConference['_id'], status: VideoConference['status']): Promise<void>;
addUser(callId: VideoConference['_id'], userId?: IUser['_id'], ts?: Date): Promise<void>;
listProviders(): Promise<{ key: string; label: string }[]>;
listCapabilities(): Promise<{ providerName: string; capabilities: VideoConferenceCapabilities }>;
listProviderCapabilities(providerName: string): Promise<VideoConferenceCapabilities>;
declineLivechatCall(callId: VideoConference['_id']): Promise<boolean>;
diagnoseProvider(uid: string, rid: string, providerName?: string): Promise<string | undefined>;
getStatistics(): Promise<IStats['videoConf']>;
validateAction(
event: string,
caller: IUser['_id'],
params: { callId: VideoConference['_id']; uid: IUser['_id']; rid: IRoom['_id'] },
): Promise<boolean>;
assignDiscussionToConference(callId: VideoConference['_id'], rid: IRoom['_id'] | undefined): Promise<void>;
createVoIP(data: InsertionModel<IVoIPVideoConference>): Promise<IVoIPVideoConference['_id'] | undefined>;
}

View File

@@ -0,0 +1,161 @@
import { EventEmitter } from 'node:events';
import type { ISetting } from '@rocket.chat/core-typings';
import type { IApiService } from './IApiService';
import type { IBroker, IBrokerNode } from './IBroker';
import type { ClientAction, EventSignatures } from '../events/Events';
import { asyncLocalStorage } from '../lib/asyncLocalStorage';
export interface IServiceContext {
id: string; // Context ID
broker: IBroker; // Instance of the broker.
nodeID: string | null; // The caller or target Node ID.
// action: Object; // Instance of action definition.
// event: Object; // Instance of event definition.
// eventName: Object; // The emitted event name.
// eventType: String; // Type of event (“emit” or “broadcast”).
// eventGroups: Array; // String> Groups of event.
// caller: String; // Service full name of the caller. E.g.: v3.myService
requestID: string | null; // Request ID. If you make nested-calls, it will be the same ID.
// parentID: String; // Parent context ID (in nested-calls).
// params: Any; // Request params. Second argument from broker.call.
// meta: Any; // Request metadata. It will be also transferred to nested-calls.
// locals: any; // Local data.
// level: Number; // Request level (in nested-calls). The first level is 1.
// span: Span; // Current active span.
ctx?: any;
}
export interface IServiceClass {
getName(): string;
onNodeConnected?({ node, reconnected }: { node: IBrokerNode; reconnected: boolean }): void;
onNodeUpdated?({ node }: { node: IBrokerNode }): void;
onNodeDisconnected?({ node, unexpected }: { node: IBrokerNode; unexpected: boolean }): Promise<void>;
getEvents(): { eventName: keyof EventSignatures; listeners: { (...args: any[]): void }[] }[];
removeAllListeners(): void;
setApi(api: IApiService): void;
onEvent<T extends keyof EventSignatures>(event: T, handler: EventSignatures[T]): void;
emit<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): void;
onSettingChanged(
settingId: ISetting['_id'],
cb: (data: { clientAction: ClientAction; setting: ISetting }) => Promise<void>,
ignoreActions?: ClientAction[],
): void;
isInternal(): boolean;
created(): Promise<void>;
started(): Promise<void>;
stopped(): Promise<void>;
}
export abstract class ServiceClass implements IServiceClass {
protected abstract name: string;
protected events = new EventEmitter();
protected settings = new EventEmitter();
protected internal = false;
protected api?: IApiService;
protected settingListenerActive = false;
constructor() {
this.emit = this.emit.bind(this);
}
setApi(api: IApiService): void {
this.api = api;
}
getEvents(): { eventName: keyof EventSignatures; listeners: { (...args: any[]): void }[] }[] {
return this.events.eventNames().map((eventName) => ({
eventName: eventName as unknown as keyof EventSignatures,
listeners: this.events.rawListeners(eventName) as { (...args: any[]): void }[],
}));
}
removeAllListeners(): void {
this.events.removeAllListeners();
this.settings.removeAllListeners();
}
getName(): string {
return this.name;
}
isInternal(): boolean {
return this.internal;
}
get context(): IServiceContext | undefined {
return asyncLocalStorage.getStore();
}
public onEvent<T extends keyof EventSignatures>(event: T, handler: EventSignatures[T]): void {
this.events.on(event, handler);
}
public emit<T extends keyof EventSignatures>(event: T, ...args: Parameters<EventSignatures[T]>): void {
this.events.emit(event, ...args);
}
private registerEventListener() {
if (this.settingListenerActive) {
return;
}
this.settingListenerActive = true;
this.onEvent('watch.settings', async ({ clientAction, setting }): Promise<void> => {
const { _id } = setting;
this.settings.emit(_id, { clientAction, setting });
});
}
public onSettingChanged(
settingId: ISetting['_id'],
cb: (data: { clientAction: ClientAction; setting: ISetting }) => Promise<void>,
ignoreActions: ClientAction[] = ['removed'],
): void {
this.registerEventListener();
this.settings.on(settingId, async ({ clientAction, setting }: { clientAction: ClientAction; setting: ISetting }): Promise<void> => {
if (ignoreActions?.includes(clientAction)) {
return;
}
try {
await cb({ clientAction, setting });
} catch {
// noop
}
});
}
async created(): Promise<void> {
// noop
}
async started(): Promise<void> {
// noop
}
async stopped(): Promise<void> {
// noop
}
}
/**
* An internal service is a service that is registered only on monolith node.
* Services that run on their own node should use @ServiceClass instead.
*/
export abstract class ServiceClassInternal extends ServiceClass {
protected override internal = true;
}

View File

@@ -0,0 +1,90 @@
import { ServiceClass } from '../src';
import { LocalBroker } from '../src/LocalBroker';
describe('LocalBroker', () => {
describe('#createService()', () => {
it('should call all the expected lifecycle hooks when creating a service', () => {
const createdStub = jest.fn();
const instance = new (class extends ServiceClass {
name = 'test';
async created() {
createdStub();
}
})();
const broker = new LocalBroker();
broker.createService(instance);
expect(createdStub).toHaveBeenCalled();
});
});
describe('#destroyService()', () => {
it('should call all the expected lifecycle hooks when destroying a service', () => {
const removeAllListenersStub = jest.fn();
const stoppedStub = jest.fn();
const instance = new (class extends ServiceClass {
name = 'test';
removeAllListeners() {
removeAllListenersStub();
}
async stopped() {
stoppedStub();
}
})();
const broker = new LocalBroker();
broker.createService(instance);
broker.destroyService(instance);
expect(removeAllListenersStub).toHaveBeenCalled();
expect(stoppedStub).toHaveBeenCalled();
});
});
describe('#broadcast()', () => {
it('should call all the ServiceClass instance registered events', () => {
const instance = new (class extends ServiceClass {
name = 'test';
})();
const testListener = jest.fn();
const testListener2 = jest.fn();
const test2Listener = jest.fn();
instance.onEvent('test' as any, testListener);
instance.onEvent('test' as any, testListener2);
instance.onEvent('test2' as any, test2Listener);
const broker = new LocalBroker();
broker.createService(instance);
broker.broadcast('test' as any, 'test');
broker.broadcast('test2' as any, 'test2');
expect(testListener).toHaveBeenCalledWith('test');
expect(testListener2).toHaveBeenCalledWith('test');
expect(test2Listener).toHaveBeenCalledWith('test2');
});
it('should NOT call any instance event anymore after the service being destroyed', () => {
const instance = new (class extends ServiceClass {
name = 'test';
})();
const testListener = jest.fn();
const test2Listener = jest.fn();
instance.onEvent('test' as any, testListener);
instance.onEvent('test2' as any, test2Listener);
const broker = new LocalBroker();
broker.createService(instance);
broker.destroyService(instance);
broker.broadcast('test' as any, 'test');
broker.broadcast('test2' as any, 'test2');
expect(testListener).not.toHaveBeenCalled();
expect(test2Listener).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,60 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { ServiceClass } from '../src/types/ServiceClass';
describe('ServiceClass', () => {
describe('#onEvent()', () => {
it('should add one event to the internal event emitter instance', () => {
const instance = new (class extends ServiceClass {})();
instance.onEvent('test' as any, () => {});
expect(instance.getEvents()).toHaveLength(1);
expect(instance.getEvents()[0].eventName).toBe('test');
});
});
describe('#getEvents()', () => {
it('should return an array of events with all of their listeners', () => {
const instance = new (class extends ServiceClass {})();
const listener1 = () => {};
const listener2 = () => {};
const listener3 = () => {};
const listeners = [listener1, listener2, listener3];
instance.onEvent('test' as any, listener1);
instance.onEvent('test' as any, listener2);
instance.onEvent('test' as any, listener3);
expect(instance.getEvents()).toHaveLength(1);
expect(instance.getEvents()[0].listeners).toEqual(listeners);
});
});
describe('#removeAllListeners()', () => {
it('should remove all listeners', () => {
const instance = new (class extends ServiceClass {})();
const listener1 = () => {};
const listener2 = () => {};
const listener3 = () => {};
instance.onEvent('test' as any, listener1);
instance.onEvent('test' as any, listener2);
instance.onEvent('test' as any, listener3);
expect(instance.getEvents()).toHaveLength(1);
instance.removeAllListeners();
expect(instance.getEvents()).toHaveLength(0);
});
it('should not be possible to receive any event after remove them all', () => {
const instance = new (class extends ServiceClass {})();
const listener1 = jest.fn();
instance.onEvent('test' as any, listener1);
instance.emit('test' as any, 'test');
instance.removeAllListeners();
instance.emit('test' as any, 'test');
expect(listener1).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,91 @@
import { ServiceStarter } from '../src/lib/ServiceStarter';
const wait = (time: number) => {
return new Promise((resolve) => {
setTimeout(() => resolve(undefined), time);
});
};
describe('ServiceStarter', () => {
it('should call the starterFn and stopperFn when calling .start and .stop', async () => {
const start = jest.fn();
const stop = jest.fn();
const instance = new ServiceStarter(start, stop);
expect(start).not.toHaveBeenCalled();
expect(stop).not.toHaveBeenCalled();
await instance.start();
expect(start).toHaveBeenCalled();
expect(stop).not.toHaveBeenCalled();
start.mockReset();
await instance.stop();
expect(start).not.toHaveBeenCalled();
expect(stop).toHaveBeenCalled();
});
it('should only call .start for the second time after the initial call has finished running', async () => {
let running = false;
const start = jest.fn(async () => {
expect(running).toBe(false);
running = true;
await wait(100);
running = false;
});
const stop = jest.fn();
const instance = new ServiceStarter(start, stop);
void instance.start();
void instance.start();
await instance.wait();
expect(start).toHaveBeenCalledTimes(2);
expect(stop).not.toHaveBeenCalled();
});
it('should chain up to two calls to .start', async () => {
const start = jest.fn(async () => {
await wait(100);
});
const stop = jest.fn();
const instance = new ServiceStarter(start, stop);
void instance.start();
void instance.start();
void instance.start();
void instance.start();
await instance.wait();
expect(start).toHaveBeenCalledTimes(2);
expect(stop).not.toHaveBeenCalled();
});
it('should skip the chained calls to .start if .stop is called', async () => {
const start = jest.fn(async () => {
await wait(100);
});
const stop = jest.fn();
const instance = new ServiceStarter(start, stop);
void instance.start();
void instance.start();
void instance.start();
void instance.stop();
await instance.wait();
expect(start).toHaveBeenCalledTimes(1);
expect(stop).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,9 @@
{
"extends": "@rocket.chat/tsconfig/server.json",
"compilerOptions": {
"declaration": true,
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src/**/*"]
}