chore: isolate callbacks and typings (#29883)

pull/29907/head^2
Guilherme Gazzo 2 years ago committed by GitHub
parent 6fd370fde8
commit db2cc31c04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      apps/meteor/app/authentication/server/startup/index.js
  2. 4
      apps/meteor/app/dolphin/server/lib.ts
  3. 4
      apps/meteor/app/e2e/server/beforeCreateRoom.ts
  4. 3
      apps/meteor/app/integrations/server/triggers.ts
  5. 10
      apps/meteor/app/irc/server/irc-bridge/index.js
  6. 12
      apps/meteor/app/lib/server/functions/createRoom.ts
  7. 7
      apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
  8. 6
      apps/meteor/app/livechat/server/startup.ts
  9. 4
      apps/meteor/client/providers/UserProvider/UserProvider.tsx
  10. 3
      apps/meteor/client/startup/afterLogoutCleanUp/customScriptOnLogout.ts
  11. 3
      apps/meteor/client/startup/afterLogoutCleanUp/purgeAllDrafts.ts
  12. 8
      apps/meteor/client/startup/afterLogoutCleanUp/roomManager.ts
  13. 4
      apps/meteor/client/startup/iframeCommands.ts
  14. 9
      apps/meteor/lib/callbacks.spec.ts
  15. 237
      apps/meteor/lib/callbacks.ts
  16. 5
      apps/meteor/lib/callbacks/afterLeaveRoomCallback.ts
  17. 3
      apps/meteor/lib/callbacks/afterLogoutCleanUpCallback.ts
  18. 6
      apps/meteor/lib/callbacks/afterRemoveFromRoomCallback.ts
  19. 6
      apps/meteor/lib/callbacks/beforeCreateRoomCallback.ts
  20. 3
      apps/meteor/lib/callbacks/beforeCreateUserCallback.ts
  21. 5
      apps/meteor/lib/callbacks/beforeLeaveRoomCallback.ts
  22. 237
      apps/meteor/lib/callbacks/callbacksBase.ts
  23. 4
      apps/meteor/server/methods/logoutCleanUp.ts
  24. 3
      apps/meteor/server/methods/removeUserFromRoom.ts
  25. 16
      apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts
  26. 111
      apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts

@ -20,6 +20,7 @@ import { safeHtmlDots } from '../../../../lib/utils/safeHtmlDots';
import { joinDefaultChannels } from '../../../lib/server/functions/joinDefaultChannels';
import { setAvatarFromServiceWithValidation } from '../../../lib/server/functions/setUserAvatar';
import { i18n } from '../../../../server/lib/i18n';
import { beforeCreateUserCallback } from '../../../../lib/callbacks/beforeCreateUserCallback';
Accounts.config({
forbidClientAccountCreation: true,
@ -158,7 +159,7 @@ const getLinkedInName = ({ firstName, lastName }) => {
};
const onCreateUserAsync = async function (options, user = {}) {
await callbacks.run('beforeCreateUser', options, user);
await beforeCreateUserCallback.run(options, user);
user.status = 'offline';
user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers');

@ -5,6 +5,7 @@ import type { IUser } from '@rocket.chat/core-typings';
import { settings } from '../../settings/server';
import { CustomOAuth } from '../../custom-oauth/server/custom_oauth_server';
import { callbacks } from '../../../lib/callbacks';
import { beforeCreateUserCallback } from '../../../lib/callbacks/beforeCreateUserCallback';
const config = {
serverURL: '',
@ -22,6 +23,7 @@ const config = {
const Dolphin = new CustomOAuth('dolphin', config);
function DolphinOnCreateUser(options: any, user?: IUser) {
// TODO: callbacks Fix this
if (user?.services?.dolphin?.NickName) {
user.username = user.services.dolphin.NickName;
}
@ -48,5 +50,5 @@ Meteor.startup(async () => {
await ServiceConfiguration.configurations.upsertAsync({ service: 'dolphin' }, { $set: data });
}
callbacks.add('beforeCreateUser', DolphinOnCreateUser, callbacks.priority.HIGH, 'dolphin');
beforeCreateUserCallback.add(DolphinOnCreateUser, callbacks.priority.HIGH, 'dolphin');
});

@ -1,7 +1,7 @@
import { callbacks } from '../../../lib/callbacks';
import { beforeCreateRoomCallback } from '../../../lib/callbacks/beforeCreateRoomCallback';
import { settings } from '../../settings/server';
callbacks.add('beforeCreateRoom', ({ type, extraData }) => {
beforeCreateRoomCallback.add(({ type, extraData }) => {
if (
settings.get<boolean>('E2E_Enable') &&
((type === 'd' && settings.get<boolean>('E2E_Enabled_Default_DirectRooms')) ||

@ -1,4 +1,5 @@
import { callbacks } from '../../../lib/callbacks';
import { afterLeaveRoomCallback } from '../../../lib/callbacks/afterLeaveRoomCallback';
import { triggerHandler } from './lib/triggerHandler';
const callbackHandler = function _callbackHandler(eventType: string) {
@ -12,6 +13,6 @@ callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), callbacks.pr
callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated');
callbacks.add('afterCreateUser', callbackHandler('userCreated'), callbacks.priority.LOW, 'integrations-userCreated');
callbacks.add('afterJoinRoom', callbackHandler('roomJoined'), callbacks.priority.LOW, 'integrations-roomJoined');
callbacks.add('afterLeaveRoom', callbackHandler('roomLeft'), callbacks.priority.LOW, 'integrations-roomLeft');
afterLeaveRoomCallback.add(callbackHandler('roomLeft'), callbacks.priority.LOW, 'integrations-roomLeft');
callbacks.add('afterRoomArchived', callbackHandler('roomArchived'), callbacks.priority.LOW, 'integrations-roomArchived');
callbacks.add('afterFileUpload', callbackHandler('fileUploaded'), callbacks.priority.LOW, 'integrations-fileUploaded');

@ -8,6 +8,8 @@ import { callbacks } from '../../../../lib/callbacks';
import * as servers from '../servers';
import { Logger } from '../../../logger/server';
import { withThrottling } from '../../../../lib/utils/highOrderFunctions';
import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback';
import { afterLogoutCleanUpCallback } from '../../../../lib/callbacks/afterLogoutCleanUpCallback';
const logger = new Logger('IRC Bridge');
const queueLogger = logger.section('Queue');
@ -204,7 +206,7 @@ class Bridge {
);
callbacks.add('afterJoinRoom', this.onMessageReceived.bind(this, 'local', 'onJoinRoom'), callbacks.priority.LOW, 'irc-on-join-room');
// Leaving rooms or channels
callbacks.add('afterLeaveRoom', this.onMessageReceived.bind(this, 'local', 'onLeaveRoom'), callbacks.priority.LOW, 'irc-on-leave-room');
afterLeaveRoomCallback.add(this.onMessageReceived.bind(this, 'local', 'onLeaveRoom'), callbacks.priority.LOW, 'irc-on-leave-room');
// Chatting
callbacks.add(
'afterSaveMessage',
@ -213,7 +215,7 @@ class Bridge {
'irc-on-save-message',
);
// Leaving
callbacks.add('afterLogoutCleanUp', this.onMessageReceived.bind(this, 'local', 'onLogout'), callbacks.priority.LOW, 'irc-on-logout');
afterLogoutCleanUpCallback.add(this.onMessageReceived.bind(this, 'local', 'onLogout'), callbacks.priority.LOW, 'irc-on-logout');
}
removeLocalHandlers() {
@ -222,9 +224,9 @@ class Bridge {
callbacks.remove('afterCreateChannel', 'irc-on-create-channel');
callbacks.remove('afterCreateRoom', 'irc-on-create-room');
callbacks.remove('afterJoinRoom', 'irc-on-join-room');
callbacks.remove('afterLeaveRoom', 'irc-on-leave-room');
afterLeaveRoomCallback.remove('irc-on-leave-room');
callbacks.remove('afterSaveMessage', 'irc-on-save-message');
callbacks.remove('afterLogoutCleanUp', 'irc-on-logout');
afterLogoutCleanUpCallback.remove('irc-on-logout');
}
sendCommand(command, parameters) {

@ -10,6 +10,7 @@ import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { callbacks } from '../../../../lib/callbacks';
import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName';
import { createDirectRoom } from './createDirectRoom';
import { beforeCreateRoomCallback } from '../../../../lib/callbacks/beforeCreateRoomCallback';
const isValidName = (name: unknown): name is string => {
return typeof name === 'string' && name.trim().length > 0;
@ -34,7 +35,16 @@ export const createRoom = async <T extends RoomType>(
}
> => {
const { teamId, ...extraData } = roomExtraData || ({} as IRoom);
await callbacks.run('beforeCreateRoom', { type, name, owner: ownerUsername, members, readOnly, extraData, options });
await beforeCreateRoomCallback.run({
type,
// name,
// owner: ownerUsername,
// members,
// readOnly,
extraData,
// options,
});
if (type === 'd') {
return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || ownerUsername });
}

@ -6,7 +6,8 @@ import { Message, Team } from '@rocket.chat/core-services';
import { Subscriptions, Rooms } from '@rocket.chat/models';
import { AppEvents, Apps } from '../../../../ee/server/apps/orchestrator';
import { callbacks } from '../../../../lib/callbacks';
import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback';
import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRoomCallback';
export const removeUserFromRoom = async function (
rid: string,
@ -29,7 +30,7 @@ export const removeUserFromRoom = async function (
throw error;
}
await callbacks.run('beforeLeaveRoom', user, room);
await beforeLeaveRoomCallback.run(user, room);
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id, {
projection: { _id: 1 },
@ -65,7 +66,7 @@ export const removeUserFromRoom = async function (
}
// TODO: CACHE: maybe a queue?
await callbacks.run('afterLeaveRoom', user, room);
await afterLeaveRoomCallback.run(user, room);
await Apps.triggerEvent(AppEvents.IPostRoomUserLeave, room, user);
};

@ -15,15 +15,15 @@ import { Livechat } from './lib/Livechat';
import { RoutingManager } from './lib/RoutingManager';
import './roomAccessValidator.internalService';
import { i18n } from '../../../server/lib/i18n';
import { beforeLeaveRoomCallback } from '../../../lib/callbacks/beforeLeaveRoomCallback';
Meteor.startup(async () => {
roomCoordinator.setRoomFind('l', (_id) => LivechatRooms.findOneById(_id));
callbacks.add(
'beforeLeaveRoom',
beforeLeaveRoomCallback.add(
function (user, room) {
if (!isOmnichannelRoom(room)) {
return user;
return;
}
throw new Meteor.Error(
i18n.t('You_cant_leave_a_livechat_room_Please_use_the_close_button', {

@ -9,7 +9,7 @@ import React, { useEffect, useMemo } from 'react';
import { Subscriptions, ChatRoom } from '../../../app/models/client';
import { getUserPreference } from '../../../app/utils/client';
import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { callbacks } from '../../../lib/callbacks';
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory';
import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning';
@ -47,7 +47,7 @@ const logout = (): Promise<void> =>
}
Meteor.logout(async () => {
await callbacks.run('afterLogoutCleanUp', user);
await afterLogoutCleanUpCallback.run(user);
sdk.call('logoutCleanUp', user).then(resolve, reject);
});
});

@ -1,8 +1,9 @@
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../lib/callbacks';
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
import { fireGlobalEvent } from '../../lib/utils/fireGlobalEvent';
Meteor.startup(() => {
callbacks.add('afterLogoutCleanUp', () => fireGlobalEvent('Custom_Script_On_Logout'), callbacks.priority.LOW, 'custom-script-on-logout');
afterLogoutCleanUpCallback.add(async () => fireGlobalEvent('Custom_Script_On_Logout'), callbacks.priority.LOW, 'custom-script-on-logout');
});

@ -1,6 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { callbacks } from '../../../lib/callbacks';
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
Meteor.startup(() => {
const purgeAllDrafts = (): void => {
@ -9,5 +10,5 @@ Meteor.startup(() => {
.forEach((key) => Meteor._localStorage.removeItem(key));
};
callbacks.add('afterLogoutCleanUp', purgeAllDrafts, callbacks.priority.MEDIUM, 'chatMessages-after-logout-cleanup');
afterLogoutCleanUpCallback.add(purgeAllDrafts, callbacks.priority.MEDIUM, 'chatMessages-after-logout-cleanup');
});

@ -2,12 +2,8 @@ import { Meteor } from 'meteor/meteor';
import { LegacyRoomManager } from '../../../app/ui-utils/client';
import { callbacks } from '../../../lib/callbacks';
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
Meteor.startup(() => {
callbacks.add(
'afterLogoutCleanUp',
() => LegacyRoomManager.closeAllRooms(),
callbacks.priority.MEDIUM,
'roommanager-after-logout-cleanup',
);
afterLogoutCleanUpCallback.add(() => LegacyRoomManager.closeAllRooms(), callbacks.priority.MEDIUM, 'roommanager-after-logout-cleanup');
});

@ -7,7 +7,7 @@ import { ServiceConfiguration } from 'meteor/service-configuration';
import { settings } from '../../app/settings/client';
import { AccountBox } from '../../app/ui-utils/client/lib/AccountBox';
import { sdk } from '../../app/utils/client/lib/SDKClient';
import { callbacks } from '../../lib/callbacks';
import { afterLogoutCleanUpCallback } from '../../lib/callbacks/afterLogoutCleanUpCallback';
import { capitalize, ltrim, rtrim } from '../../lib/utils/stringUtils';
import { baseURI } from '../lib/baseURI';
import { router } from '../providers/RouterProvider';
@ -80,7 +80,7 @@ const commands = {
if (!user) {
return;
}
void callbacks.run('afterLogoutCleanUp', user);
void afterLogoutCleanUpCallback.run(user);
sdk.call('logoutCleanUp', user as unknown as IUser);
return router.navigate('/home');
});

@ -1,7 +1,8 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { callbacks, Callbacks } from './callbacks';
import { callbacks } from './callbacks';
import { Callbacks } from './callbacks/callbacksBase';
describe('callbacks legacy', () => {
it("if the callback doesn't return any value should return the original", async () => {
@ -37,7 +38,7 @@ describe('callbacks legacy', () => {
describe('callbacks', () => {
it("if the callback doesn't return any value should return the original", async () => {
const test = Callbacks.create<boolean, boolean>('test');
const test = Callbacks.create<(data: boolean) => boolean>('test');
test.add(() => undefined, callbacks.priority.LOW, '1');
@ -47,7 +48,7 @@ describe('callbacks', () => {
});
it('should return the value returned by the callback', async () => {
const test = Callbacks.create<boolean, boolean>('test');
const test = Callbacks.create<(data: boolean) => boolean>('test');
test.add(() => false, callbacks.priority.LOW, '1');
@ -57,7 +58,7 @@ describe('callbacks', () => {
});
it('should accumulate the values returned by the callbacks', async () => {
const test = Callbacks.create<number, number>('test');
const test = Callbacks.create<(data: number) => number>('test');
test.add((old) => old * 5);

@ -21,19 +21,11 @@ import type {
InquiryWithAgentInfo,
ILivechatTagRecord,
} from '@rocket.chat/core-typings';
import { Random } from '@rocket.chat/random';
import type { Logger } from '../app/logger/server';
import type { IBusinessHourBehavior } from '../app/livechat/server/business-hour/AbstractBusinessHour';
import type { ILoginAttempt } from '../app/authentication/server/ILoginAttempt';
import { compareByRanking } from './utils/comparisons';
import type { CloseRoomParams } from '../app/livechat/server/lib/LivechatTyped';
enum CallbackPriority {
HIGH = -1000,
MEDIUM = 0,
LOW = 1000,
}
import { Callbacks } from './callbacks/callbacksBase';
/**
* Callbacks returning void, like event listeners.
@ -84,7 +76,6 @@ interface EventLikeCallbackSignatures {
'federation.onAddUsersToARoom': (params: { invitees: IUser[] | Username[]; inviter: IUser }, room: IRoom) => void;
'onJoinVideoConference': (callId: VideoConference['_id'], userId?: IUser['_id']) => Promise<void>;
'usernameSet': () => void;
'beforeLeaveRoom': (user: IUser, room: IRoom) => void;
'beforeJoinRoom': (user: IUser, room: IRoom) => void;
'beforeMuteUser': (users: { mutedUser: IUser; fromUser: IUser }, room: IRoom) => void;
'afterMuteUser': (users: { mutedUser: IUser; fromUser: IUser }, room: IRoom) => void;
@ -92,7 +83,6 @@ interface EventLikeCallbackSignatures {
'afterUnmuteUser': (users: { mutedUser: IUser; fromUser: IUser }, room: IRoom) => void;
'afterValidateLogin': (login: { user: IUser }) => void;
'afterJoinRoom': (user: IUser, room: IRoom) => void;
'beforeCreateRoom': (data: { type: IRoom['t']; extraData: { encrypted: boolean } }) => void;
'livechat.afterDepartmentDisabled': (department: ILivechatDepartmentRecord) => void;
'livechat.afterDepartmentArchived': (department: Pick<ILivechatDepartmentRecord, '_id'>) => void;
'afterSaveUser': ({ user, oldUser }: { user: IUser; oldUser: IUser | null }) => void;
@ -220,16 +210,12 @@ type ChainedCallbackSignatures = {
export type Hook =
| keyof EventLikeCallbackSignatures
| keyof ChainedCallbackSignatures
| 'afterLeaveRoom'
| 'afterLogoutCleanUp'
| 'afterProcessOAuthUser'
| 'afterRemoveFromRoom'
| 'afterRoomArchived'
| 'afterRoomTopicChange'
| 'afterSaveUser'
| 'afterValidateNewOAuthUser'
| 'beforeActivateUser'
| 'beforeCreateUser'
| 'beforeGetMentions'
| 'beforeReadMessages'
| 'beforeRemoveFromRoom'
@ -265,218 +251,17 @@ export type Hook =
| 'userStatusManuallySet'
| 'test';
type Callback = {
(item: unknown, constant?: unknown): Promise<unknown>;
hook: Hook;
id: string;
priority: CallbackPriority;
stack: string;
};
type CallbackTracker = (callback: Callback) => () => void;
type HookTracker = (params: { hook: Hook; length: number }) => () => void;
export class Callbacks {
private logger: Logger | undefined = undefined;
private trackCallback: CallbackTracker | undefined = undefined;
private trackHook: HookTracker | undefined = undefined;
private callbacks = new Map<Hook, Callback[]>();
private sequentialRunners = new Map<Hook, (item: unknown, constant?: unknown) => Promise<unknown>>();
private asyncRunners = new Map<Hook, (item: unknown, constant?: unknown) => unknown>();
readonly priority = CallbackPriority;
setLogger(logger: Logger): void {
this.logger = logger;
}
setMetricsTrackers({ trackCallback, trackHook }: { trackCallback?: CallbackTracker; trackHook?: HookTracker }): void {
this.trackCallback = trackCallback;
this.trackHook = trackHook;
}
private runOne(callback: Callback, item: unknown, constant: unknown): Promise<unknown> {
const stopTracking = this.trackCallback?.(callback);
return Promise.resolve(callback(item, constant)).finally(stopTracking);
}
private createSequentialRunner(hook: Hook, callbacks: Callback[]): (item: unknown, constant?: unknown) => Promise<unknown> {
const wrapCallback =
(callback: Callback) =>
async (item: unknown, constant?: unknown): Promise<unknown> => {
this.logger?.debug(`Executing callback with id ${callback.id} for hook ${callback.hook}`);
return (await this.runOne(callback, item, constant)) ?? item;
};
const identity = <TItem>(item: TItem): Promise<TItem> => Promise.resolve(item);
const pipe =
(curr: (item: unknown, constant?: unknown) => Promise<unknown>, next: (item: unknown, constant?: unknown) => Promise<unknown>) =>
async (item: unknown, constant?: unknown): Promise<unknown> =>
next(await curr(item, constant), constant);
const fn = callbacks.map(wrapCallback).reduce(pipe, identity);
return async (item: unknown, constant?: unknown): Promise<unknown> => {
const stopTracking = this.trackHook?.({ hook, length: callbacks.length });
return fn(item, constant).finally(() => stopTracking?.());
};
}
private createAsyncRunner(_: Hook, callbacks: Callback[]) {
return (item: unknown, constant?: unknown): unknown => {
if (typeof window !== 'undefined') {
throw new Error('callbacks.runAsync on client server not allowed');
}
for (const callback of callbacks) {
setTimeout(() => {
void this.runOne(callback, item, constant);
}, 0);
}
return item;
};
}
getCallbacks(hook: Hook): Callback[] {
return this.callbacks.get(hook) ?? [];
}
setCallbacks(hook: Hook, callbacks: Callback[]): void {
this.callbacks.set(hook, callbacks);
this.sequentialRunners.set(hook, this.createSequentialRunner(hook, callbacks));
this.asyncRunners.set(hook, this.createAsyncRunner(hook, callbacks));
}
/**
* Add a callback function to a hook
*
* @param hook the name of the hook
* @param callback the callback function
* @param priority the callback run priority (order)
* @param id human friendly name for this callback
*/
add<THook extends keyof EventLikeCallbackSignatures>(
hook: THook,
callback: EventLikeCallbackSignatures[THook],
priority?: CallbackPriority,
id?: string,
): void;
add<THook extends keyof ChainedCallbackSignatures>(
hook: THook,
callback: ChainedCallbackSignatures[THook],
priority?: CallbackPriority,
id?: string,
): void;
add<TItem, TConstant, TNextItem = TItem>(
hook: Hook,
callback: (item: TItem, constant?: TConstant) => TNextItem,
priority?: CallbackPriority,
id?: string,
): void;
add(hook: Hook, callback: (item: unknown, constant?: unknown) => unknown, priority = this.priority.MEDIUM, id = Random.id()): void {
const callbacks = this.getCallbacks(hook);
if (callbacks.some((cb) => cb.id === id)) {
return;
}
callbacks.push(
Object.assign(callback as Callback, {
hook,
priority,
id,
stack: new Error().stack,
}),
);
callbacks.sort(compareByRanking((callback: Callback): number => callback.priority ?? this.priority.MEDIUM));
this.setCallbacks(hook, callbacks);
}
/**
* Remove a callback from a hook
*
* @param hook the name of the hook
* @param id the callback's id
*/
remove(hook: Hook, id: string): void {
const hooks = this.getCallbacks(hook).filter((callback) => callback.id !== id);
this.setCallbacks(hook, hooks);
}
run<THook extends keyof EventLikeCallbackSignatures>(hook: THook, ...args: Parameters<EventLikeCallbackSignatures[THook]>): void;
run<THook extends keyof ChainedCallbackSignatures>(
hook: THook,
...args: Parameters<ChainedCallbackSignatures[THook]>
): Promise<ReturnType<ChainedCallbackSignatures[THook]>>;
run<TItem, TConstant, TNextItem = TItem>(hook: Hook, item: TItem, constant?: TConstant): Promise<TNextItem>;
/**
* Successively run all of a hook's callbacks on an item
*
* @param hook the name of the hook
* @param item the post, comment, modifier, etc. on which to run the callbacks
* @param constant an optional constant that will be passed along to each callback
* @returns returns the item after it's been through all the callbacks for this hook
*/
run(hook: Hook, item: unknown, constant?: unknown): Promise<unknown> {
const runner = this.sequentialRunners.get(hook) ?? (async (item: unknown, _constant?: unknown): Promise<unknown> => item);
return runner(item, constant);
}
runAsync<THook extends keyof EventLikeCallbackSignatures>(hook: THook, ...args: Parameters<EventLikeCallbackSignatures[THook]>): void;
/**
* Successively run all of a hook's callbacks on an item, in async mode (only works on server)
*
* @param hook the name of the hook
* @param item the post, comment, modifier, etc. on which to run the callbacks
* @param constant an optional constant that will be passed along to each callback
* @returns the post, comment, modifier, etc. on which to run the callbacks
*/
runAsync(hook: Hook, item: unknown, constant?: unknown): unknown {
const runner = this.asyncRunners.get(hook) ?? ((item: unknown, _constant?: unknown): unknown => item);
return runner(item, constant);
}
static create<I, R, C = undefined>(hook: string): Cb<I, R, C> {
const callbacks = new Callbacks();
return {
add: (callback, priority, id) => callbacks.add(hook as any, callback, priority, id),
remove: (id) => callbacks.remove(hook as any, id),
run: (item, constant) => callbacks.run(hook as any, item, constant) as any,
};
}
}
/**
* Callback hooks provide an easy way to add extra steps to common operations.
* @deprecated
*/
type Cb<I, R, C = undefined> = {
add: (callback: (item: I, constant?: C) => R | undefined, priority?: CallbackPriority, id?: string) => void;
remove: (id: string) => void;
run: (item: I, constant?: C) => Promise<R>;
};
/**
* Callback hooks provide an easy way to add extra steps to common operations.
* @deprecated
*/
export const callbacks = new Callbacks();
export const callbacks = new Callbacks<
{
[key in keyof ChainedCallbackSignatures]: ChainedCallbackSignatures[key];
},
{
[key in keyof EventLikeCallbackSignatures]: EventLikeCallbackSignatures[key];
},
Hook
>();

@ -0,0 +1,5 @@
import type { IUser, IRoom } from '@rocket.chat/core-typings';
import { Callbacks } from './callbacksBase';
export const afterLeaveRoomCallback = Callbacks.create<(user: IUser, room: IRoom) => void>('afterLeaveRoom');

@ -0,0 +1,3 @@
import { Callbacks } from './callbacksBase';
export const afterLogoutCleanUpCallback = Callbacks.create('afterLogoutCleanUp');

@ -0,0 +1,6 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { Callbacks } from './callbacksBase';
export const afterRemoveFromRoomCallback =
Callbacks.create<(data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom) => void>('afterRemoveFromRoom');

@ -0,0 +1,6 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { Callbacks } from './callbacksBase';
export const beforeCreateRoomCallback =
Callbacks.create<(data: { type: IRoom['t']; extraData: { encrypted?: boolean } }) => void>('beforeCreateRoom');

@ -0,0 +1,3 @@
import { Callbacks } from './callbacksBase';
export const beforeCreateUserCallback = Callbacks.create('beforeCreateUser');

@ -0,0 +1,5 @@
import type { IRoom, IUser } from '@rocket.chat/core-typings';
import { Callbacks } from './callbacksBase';
export const beforeLeaveRoomCallback = Callbacks.create<(user: IUser, room: IRoom) => void>('beforeLeaveRoom');

@ -0,0 +1,237 @@
import { Random } from '@rocket.chat/random';
import { compareByRanking } from '../utils/comparisons';
import type { Logger } from '../../app/logger/server';
enum CallbackPriority {
HIGH = -1000,
MEDIUM = 0,
LOW = 1000,
}
type Callback<H> = {
(item: unknown, constant?: unknown): Promise<unknown>;
hook: H;
id: string;
priority: CallbackPriority;
stack: string;
};
type CallbackTracker<H> = (callback: Callback<H>) => () => void;
type HookTracker<H> = (params: { hook: H; length: number }) => () => void;
export class Callbacks<
TChainedCallbackSignatures extends {
[key: string]: (item: any, constant?: any) => any;
},
TEventLikeCallbackSignatures extends {
[key: string]: (item: any, constant?: any) => any;
},
THook extends string = keyof TChainedCallbackSignatures & keyof TEventLikeCallbackSignatures & string,
> {
private logger: Logger | undefined = undefined;
private trackCallback: CallbackTracker<THook> | undefined = undefined;
private trackHook: HookTracker<THook> | undefined = undefined;
private callbacks = new Map<THook, Callback<THook>[]>();
private sequentialRunners = new Map<THook, (item: unknown, constant?: unknown) => Promise<unknown>>();
private asyncRunners = new Map<THook, (item: unknown, constant?: unknown) => unknown>();
readonly priority = CallbackPriority;
setLogger(logger: Logger): void {
this.logger = logger;
}
setMetricsTrackers({ trackCallback, trackHook }: { trackCallback?: CallbackTracker<THook>; trackHook?: HookTracker<THook> }): void {
this.trackCallback = trackCallback;
this.trackHook = trackHook;
}
private runOne(callback: Callback<THook>, item: unknown, constant: unknown): Promise<unknown> {
const stopTracking = this.trackCallback?.(callback);
return Promise.resolve(callback(item, constant)).finally(stopTracking);
}
private createSequentialRunner(hook: THook, callbacks: Callback<THook>[]): (item: unknown, constant?: unknown) => Promise<unknown> {
const wrapCallback =
(callback: Callback<THook>) =>
async (item: unknown, constant?: unknown): Promise<unknown> => {
this.logger?.debug(`Executing callback with id ${callback.id} for hook ${callback.hook}`);
return (await this.runOne(callback, item, constant)) ?? item;
};
const identity = <TItem>(item: TItem): Promise<TItem> => Promise.resolve(item);
const pipe =
(curr: (item: unknown, constant?: unknown) => Promise<unknown>, next: (item: unknown, constant?: unknown) => Promise<unknown>) =>
async (item: unknown, constant?: unknown): Promise<unknown> =>
next(await curr(item, constant), constant);
const fn = callbacks.map(wrapCallback).reduce(pipe, identity);
return async (item: unknown, constant?: unknown): Promise<unknown> => {
const stopTracking = this.trackHook?.({ hook, length: callbacks.length });
return fn(item, constant).finally(() => stopTracking?.());
};
}
private createAsyncRunner(_: THook, callbacks: Callback<THook>[]) {
return (item: unknown, constant?: unknown): unknown => {
if (typeof window !== 'undefined') {
throw new Error('callbacks.runAsync on client server not allowed');
}
for (const callback of callbacks) {
setTimeout(() => {
void this.runOne(callback, item, constant);
}, 0);
}
return item;
};
}
getCallbacks(hook: THook): Callback<THook>[] {
return this.callbacks.get(hook) ?? [];
}
setCallbacks(hook: THook, callbacks: Callback<THook>[]): void {
this.callbacks.set(hook, callbacks);
this.sequentialRunners.set(hook, this.createSequentialRunner(hook, callbacks));
this.asyncRunners.set(hook, this.createAsyncRunner(hook, callbacks));
}
/**
* Add a callback function to a hook
*
* @param hook the name of the hook
* @param callback the callback function
* @param priority the callback run priority (order)
* @param id human friendly name for this callback
*/
add<Hook extends keyof TEventLikeCallbackSignatures>(
hook: Hook,
callback: TEventLikeCallbackSignatures[Hook],
priority?: CallbackPriority,
id?: string,
): void;
add<Hook extends keyof TChainedCallbackSignatures>(
hook: Hook,
callback: TChainedCallbackSignatures[Hook],
priority?: CallbackPriority,
id?: string,
): void;
add<TItem, TConstant, TNextItem = TItem>(
hook: THook,
callback: (item: TItem, constant?: TConstant) => TNextItem,
priority?: CallbackPriority,
id?: string,
): void;
add(hook: THook, callback: (item: unknown, constant?: unknown) => unknown, priority = this.priority.MEDIUM, id = Random.id()): void {
const callbacks = this.getCallbacks(hook);
if (callbacks.some((cb) => cb.id === id)) {
return;
}
callbacks.push(
Object.assign(callback as Callback<THook>, {
hook,
priority,
id,
stack: new Error().stack,
}),
);
callbacks.sort(compareByRanking((callback: Callback<THook>): number => callback.priority ?? this.priority.MEDIUM));
this.setCallbacks(hook, callbacks);
}
/**
* Remove a callback from a hook
*
* @param hook the name of the hook
* @param id the callback's id
*/
remove(hook: THook, id: string): void {
const hooks = this.getCallbacks(hook).filter((callback) => callback.id !== id);
this.setCallbacks(hook, hooks);
}
run<Hook extends keyof TEventLikeCallbackSignatures>(hook: Hook, ...args: Parameters<TEventLikeCallbackSignatures[Hook]>): void;
run<Hook extends keyof TChainedCallbackSignatures>(
hook: Hook,
...args: Parameters<TChainedCallbackSignatures[Hook]>
): Promise<ReturnType<TChainedCallbackSignatures[Hook]>>;
run<TItem, TConstant, TNextItem = TItem>(hook: THook, item: TItem, constant?: TConstant): Promise<TNextItem>;
/**
* Successively run all of a hook's callbacks on an item
*
* @param hook the name of the hook
* @param item the post, comment, modifier, etc. on which to run the callbacks
* @param constant an optional constant that will be passed along to each callback
* @returns returns the item after it's been through all the callbacks for this hook
*/
run(hook: THook, item: unknown, constant?: unknown): Promise<unknown> {
const runner = this.sequentialRunners.get(hook) ?? (async (item: unknown, _constant?: unknown): Promise<unknown> => item);
return runner(item, constant);
}
runAsync<Hook extends keyof TEventLikeCallbackSignatures>(hook: Hook, ...args: Parameters<TEventLikeCallbackSignatures[Hook]>): void;
/**
* Successively run all of a hook's callbacks on an item, in async mode (only works on server)
*
* @param hook the name of the hook
* @param item the post, comment, modifier, etc. on which to run the callbacks
* @param constant an optional constant that will be passed along to each callback
* @returns the post, comment, modifier, etc. on which to run the callbacks
*/
runAsync(hook: THook, item: unknown, constant?: unknown): unknown {
const runner = this.asyncRunners.get(hook) ?? ((item: unknown, _constant?: unknown): unknown => item);
return runner(item, constant);
}
static create<F extends (item: any, constant?: any) => any | Promise<any>>(
hook: string,
): Cb<Parameters<F>[0], ReturnType<F>, Parameters<F>[1]>;
static create<I, R, C = undefined>(hook: string): Cb<I, R, C> {
const callbacks = new Callbacks();
return {
add: (callback, priority, id) => callbacks.add(hook as any, callback, priority, id),
remove: (id) => callbacks.remove(hook as any, id),
run: (item, constant) => callbacks.run(hook as any, item, constant) as any,
};
}
}
/**
* Callback hooks provide an easy way to add extra steps to common operations.
* @deprecated
*/
type Cb<I, R, C = undefined> = {
add: (
callback: (item: I, constant: C) => Promise<R | undefined | void> | R | undefined | void,
priority?: CallbackPriority,
id?: string,
) => void;
remove: (id: string) => void;
run: (item: I, constant?: C) => Promise<R>;
};

@ -3,8 +3,8 @@ import { check } from 'meteor/check';
import type { IUser } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { callbacks } from '../../lib/callbacks';
import { AppEvents, Apps } from '../../ee/server/apps/orchestrator';
import { afterLogoutCleanUpCallback } from '../../lib/callbacks/afterLogoutCleanUpCallback';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -18,7 +18,7 @@ Meteor.methods<ServerMethods>({
check(user, Object);
setImmediate(() => {
void callbacks.run('afterLogoutCleanUp', user);
void afterLogoutCleanUpCallback.run(user);
});
// App IPostUserLogout event hook

@ -11,6 +11,7 @@ import { callbacks } from '../../lib/callbacks';
import { roomCoordinator } from '../lib/rooms/roomCoordinator';
import { RoomMemberActions } from '../../definition/IRoomTypeConfig';
import { getUsersInRole } from '../../app/authorization/server';
import { afterRemoveFromRoomCallback } from '../../lib/callbacks/afterRemoveFromRoomCallback';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -78,7 +79,7 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri
}
setImmediate(function () {
void callbacks.run('afterRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room);
void afterRemoveFromRoomCallback.run({ removedUser, userWhoRemoved: fromUser }, room);
});
return true;

@ -4,12 +4,13 @@ import { isMessageFromMatrixFederation, isRoomFederated, isEditedMessage } from
import type { FederationRoomServiceSender } from '../../../application/room/sender/RoomServiceSender';
import { settings } from '../../../../../../app/settings/server';
import { callbacks } from '../../../../../../lib/callbacks';
import { afterLeaveRoomCallback } from '../../../../../../lib/callbacks/afterLeaveRoomCallback';
import { afterRemoveFromRoomCallback } from '../../../../../../lib/callbacks/afterRemoveFromRoomCallback';
export class FederationHooks {
public static afterUserLeaveRoom(callback: (user: IUser, room: IRoom) => Promise<void>): void {
callbacks.add(
'afterLeaveRoom',
async (user: IUser, room: IRoom | undefined): Promise<void> => {
afterLeaveRoomCallback.add(
async (user: IUser, room?: IRoom): Promise<void> => {
if (!room || !isRoomFederated(room) || !user || !settings.get('Federation_Matrix_enabled')) {
return;
}
@ -21,9 +22,8 @@ export class FederationHooks {
}
public static onUserRemovedFromRoom(callback: (removedUser: IUser, room: IRoom, userWhoRemoved: IUser) => Promise<void>): void {
callbacks.add(
'afterRemoveFromRoom',
async (params: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom | undefined): Promise<void> => {
afterRemoveFromRoomCallback.add(
async (params, room): Promise<void> => {
if (
!room ||
!isRoomFederated(room) ||
@ -255,8 +255,8 @@ export class FederationHooks {
}
public static removeAllListeners(): void {
callbacks.remove('afterLeaveRoom', 'federation-v2-after-leave-room');
callbacks.remove('afterRemoveFromRoom', 'federation-v2-after-remove-from-room');
afterLeaveRoomCallback.remove('federation-v2-after-leave-room');
afterRemoveFromRoomCallback.remove('federation-v2-after-remove-from-room');
callbacks.remove('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-non-federated-room');
callbacks.remove('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-federated-room');
callbacks.remove('federation.beforeCreateDirectMessage', 'federation-v2-can-create-direct-message-from-ui-ce');

@ -9,6 +9,8 @@ const get = sinon.stub();
const hooks: Record<string, any> = {};
import type * as hooksModule from '../../../../../../../server/services/federation/infrastructure/rocket-chat/hooks';
import { afterLeaveRoomCallback } from '../../../../../../../lib/callbacks/afterLeaveRoomCallback';
import { afterRemoveFromRoomCallback } from '../../../../../../../lib/callbacks/afterRemoveFromRoomCallback';
const { FederationHooks } = proxyquire
.noCallThru()
@ -28,113 +30,150 @@ const { FederationHooks } = proxyquire
},
},
},
'../../../../../../lib/callbacks/afterLeaveRoomCallback': {
afterLeaveRoomCallback,
},
'../../../../../../lib/callbacks/afterRemoveFromRoomCallback': {
afterRemoveFromRoomCallback,
},
'../../../../../../app/settings/server': {
settings: { get },
},
});
describe('Federation - Infrastructure - RocketChat - Hooks', () => {
afterEach(() => {
beforeEach(() => {
FederationHooks.removeAllListeners();
remove.reset();
get.reset();
});
describe('#afterUserLeaveRoom()', () => {
it('should NOT execute the callback if no room was provided', () => {
it('should NOT execute the callback if no room was provided', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.afterUserLeaveRoom(stub);
hooks['federation-v2-after-leave-room']();
// @ts-expect-error
await afterLeaveRoomCallback.run();
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if the provided room is not federated', () => {
it('should NOT execute the callback if the provided room is not federated', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.afterUserLeaveRoom(stub);
hooks['federation-v2-after-leave-room']({}, {});
// @ts-expect-error
await afterLeaveRoomCallback.run({}, {});
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if no user was provided', () => {
it('should NOT execute the callback if no user was provided', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.afterUserLeaveRoom(stub);
hooks['federation-v2-after-leave-room'](undefined, { federated: true });
// @ts-expect-error
await afterLeaveRoomCallback.run(undefined, { federated: true });
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if federation module was disabled', () => {
it('should NOT execute the callback if federation module was disabled', async () => {
get.returns(false);
const stub = sinon.stub();
FederationHooks.afterUserLeaveRoom(stub);
hooks['federation-v2-after-leave-room']({}, { federated: true });
// @ts-expect-error
await afterLeaveRoomCallback.run({}, { federated: true });
expect(stub.called).to.be.false;
});
it('should execute the callback when everything is correct', () => {
it('should execute the callback when everything is correct', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.afterUserLeaveRoom(stub);
hooks['federation-v2-after-leave-room']({}, { federated: true });
// @ts-expect-error
await afterLeaveRoomCallback.run({}, { federated: true });
expect(stub.calledWith({}, { federated: true })).to.be.true;
});
});
describe('#onUserRemovedFromRoom()', () => {
it('should NOT execute the callback if no room was provided', () => {
it('should NOT execute the callback if no room was provided', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']();
// @ts-expect-error
await afterRemoveFromRoomCallback.run();
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if the provided room is not federated', () => {
it('should NOT execute the callback if the provided room is not federated', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']({}, {});
// @ts-expect-error
await afterRemoveFromRoomCallback.run({}, {});
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if no params were provided', () => {
it('should NOT execute the callback if no params were provided', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']({}, { federated: true });
// @ts-expect-error
await afterRemoveFromRoomCallback.run({}, { federated: true });
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if no removedUser was provided', () => {
it('should NOT execute the callback if no removedUser was provided', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']({}, { federated: true }, {});
// @ts-expect-error
await afterRemoveFromRoomCallback.run({}, { federated: true }, {});
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if no userWhoRemoved was provided', () => {
it('should NOT execute the callback if no userWhoRemoved was provided', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']({ removedUser: 'removedUser' }, { federated: true });
// @ts-expect-error
await afterRemoveFromRoomCallback.run({ removedUser: 'removedUser' }, { federated: true });
expect(stub.called).to.be.false;
});
it('should NOT execute the callback if federation module was disabled', () => {
it('should NOT execute the callback if federation module was disabled', async () => {
get.returns(false);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']({ removedUser: 'removedUser', userWhoRemoved: 'userWhoRemoved' }, { federated: true });
// @ts-expect-error
await afterRemoveFromRoomCallback.run({ removedUser: 'removedUser', userWhoRemoved: 'userWhoRemoved' }, { federated: true });
expect(stub.called).to.be.false;
});
it('should execute the callback when everything is correct', () => {
it('should execute the callback when everything is correct', async () => {
get.returns(true);
const stub = sinon.stub();
FederationHooks.onUserRemovedFromRoom(stub);
hooks['federation-v2-after-remove-from-room']({ removedUser: 'removedUser', userWhoRemoved: 'userWhoRemoved' }, { federated: true });
// @ts-expect-error
await afterRemoveFromRoomCallback.run({ removedUser: 'removedUser', userWhoRemoved: 'userWhoRemoved' }, { federated: true });
expect(stub.calledWith('removedUser', { federated: true }, 'userWhoRemoved')).to.be.true;
});
});
@ -696,24 +735,22 @@ describe('Federation - Infrastructure - RocketChat - Hooks', () => {
describe('#removeAllListeners()', () => {
it('should remove all the listeners', () => {
FederationHooks.removeAllListeners();
expect(remove.callCount).to.be.equal(11);
expect(remove.getCall(0).calledWith('afterLeaveRoom', 'federation-v2-after-leave-room')).to.be.equal(true);
expect(remove.getCall(1).calledWith('afterRemoveFromRoom', 'federation-v2-after-remove-from-room')).to.be.equal(true);
expect(remove.callCount).to.be.equal(9);
expect(
remove.getCall(2).calledWith('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-non-federated-room'),
remove.getCall(0).calledWith('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-non-federated-room'),
).to.be.equal(true);
expect(
remove.getCall(3).calledWith('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-federated-room'),
remove.getCall(1).calledWith('federation.beforeAddUserToARoom', 'federation-v2-can-add-federated-user-to-federated-room'),
).to.be.equal(true);
expect(
remove.getCall(4).calledWith('federation.beforeCreateDirectMessage', 'federation-v2-can-create-direct-message-from-ui-ce'),
remove.getCall(2).calledWith('federation.beforeCreateDirectMessage', 'federation-v2-can-create-direct-message-from-ui-ce'),
).to.be.equal(true);
expect(remove.getCall(5).calledWith('afterSetReaction', 'federation-v2-after-message-reacted')).to.be.equal(true);
expect(remove.getCall(6).calledWith('afterUnsetReaction', 'federation-v2-after-message-unreacted')).to.be.equal(true);
expect(remove.getCall(7).calledWith('afterDeleteMessage', 'federation-v2-after-room-message-deleted')).to.be.equal(true);
expect(remove.getCall(8).calledWith('afterSaveMessage', 'federation-v2-after-room-message-updated')).to.be.equal(true);
expect(remove.getCall(9).calledWith('afterSaveMessage', 'federation-v2-after-room-message-sent')).to.be.equal(true);
expect(remove.getCall(10).calledWith('afterSaveMessage', 'federation-v2-after-room-message-sent')).to.be.equal(true);
expect(remove.getCall(3).calledWith('afterSetReaction', 'federation-v2-after-message-reacted')).to.be.equal(true);
expect(remove.getCall(4).calledWith('afterUnsetReaction', 'federation-v2-after-message-unreacted')).to.be.equal(true);
expect(remove.getCall(5).calledWith('afterDeleteMessage', 'federation-v2-after-room-message-deleted')).to.be.equal(true);
expect(remove.getCall(6).calledWith('afterSaveMessage', 'federation-v2-after-room-message-updated')).to.be.equal(true);
expect(remove.getCall(7).calledWith('afterSaveMessage', 'federation-v2-after-room-message-sent')).to.be.equal(true);
expect(remove.getCall(8).calledWith('afterSaveMessage', 'federation-v2-after-room-message-sent')).to.be.equal(true);
});
});
});

Loading…
Cancel
Save