diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 726518465e9..0c391535513 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -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'); diff --git a/apps/meteor/app/dolphin/server/lib.ts b/apps/meteor/app/dolphin/server/lib.ts index f6693807996..bb00b684b3b 100644 --- a/apps/meteor/app/dolphin/server/lib.ts +++ b/apps/meteor/app/dolphin/server/lib.ts @@ -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'); }); diff --git a/apps/meteor/app/e2e/server/beforeCreateRoom.ts b/apps/meteor/app/e2e/server/beforeCreateRoom.ts index 6c71cb84b1b..a9577204b3c 100644 --- a/apps/meteor/app/e2e/server/beforeCreateRoom.ts +++ b/apps/meteor/app/e2e/server/beforeCreateRoom.ts @@ -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('E2E_Enable') && ((type === 'd' && settings.get('E2E_Enabled_Default_DirectRooms')) || diff --git a/apps/meteor/app/integrations/server/triggers.ts b/apps/meteor/app/integrations/server/triggers.ts index eedc98f71af..cdf8acda6a2 100644 --- a/apps/meteor/app/integrations/server/triggers.ts +++ b/apps/meteor/app/integrations/server/triggers.ts @@ -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'); diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index e83e99c8c3f..5e4a508e8e9 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -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) { diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index 836cb8917cb..cff656e70cc 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -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 ( } > => { 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 }); } diff --git a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts index 91c20ab6715..4c73f70e6c5 100644 --- a/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts +++ b/apps/meteor/app/lib/server/functions/removeUserFromRoom.ts @@ -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); }; diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts index d8c5ba098d6..f694c7b229f 100644 --- a/apps/meteor/app/livechat/server/startup.ts +++ b/apps/meteor/app/livechat/server/startup.ts @@ -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', { diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 84a1efe8f2d..d4f982d3350 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -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 => } Meteor.logout(async () => { - await callbacks.run('afterLogoutCleanUp', user); + await afterLogoutCleanUpCallback.run(user); sdk.call('logoutCleanUp', user).then(resolve, reject); }); }); diff --git a/apps/meteor/client/startup/afterLogoutCleanUp/customScriptOnLogout.ts b/apps/meteor/client/startup/afterLogoutCleanUp/customScriptOnLogout.ts index 92bd47341dd..ba2394fc7d6 100644 --- a/apps/meteor/client/startup/afterLogoutCleanUp/customScriptOnLogout.ts +++ b/apps/meteor/client/startup/afterLogoutCleanUp/customScriptOnLogout.ts @@ -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'); }); diff --git a/apps/meteor/client/startup/afterLogoutCleanUp/purgeAllDrafts.ts b/apps/meteor/client/startup/afterLogoutCleanUp/purgeAllDrafts.ts index ef25b0ffe2d..d0c15c93263 100644 --- a/apps/meteor/client/startup/afterLogoutCleanUp/purgeAllDrafts.ts +++ b/apps/meteor/client/startup/afterLogoutCleanUp/purgeAllDrafts.ts @@ -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'); }); diff --git a/apps/meteor/client/startup/afterLogoutCleanUp/roomManager.ts b/apps/meteor/client/startup/afterLogoutCleanUp/roomManager.ts index 4f9f5499b79..3967617a82b 100644 --- a/apps/meteor/client/startup/afterLogoutCleanUp/roomManager.ts +++ b/apps/meteor/client/startup/afterLogoutCleanUp/roomManager.ts @@ -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'); }); diff --git a/apps/meteor/client/startup/iframeCommands.ts b/apps/meteor/client/startup/iframeCommands.ts index 25836a38f5c..14c0e683d7a 100644 --- a/apps/meteor/client/startup/iframeCommands.ts +++ b/apps/meteor/client/startup/iframeCommands.ts @@ -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'); }); diff --git a/apps/meteor/lib/callbacks.spec.ts b/apps/meteor/lib/callbacks.spec.ts index 4459ec1cf47..e5b41950742 100644 --- a/apps/meteor/lib/callbacks.spec.ts +++ b/apps/meteor/lib/callbacks.spec.ts @@ -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('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('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('test'); + const test = Callbacks.create<(data: number) => number>('test'); test.add((old) => old * 5); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index ce4c8517d2d..ed4d44c65be 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -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; '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) => 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; - 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(); - - private sequentialRunners = new Map Promise>(); - - private asyncRunners = new Map 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 { - const stopTracking = this.trackCallback?.(callback); - - return Promise.resolve(callback(item, constant)).finally(stopTracking); - } - - private createSequentialRunner(hook: Hook, callbacks: Callback[]): (item: unknown, constant?: unknown) => Promise { - const wrapCallback = - (callback: Callback) => - async (item: unknown, constant?: unknown): Promise => { - this.logger?.debug(`Executing callback with id ${callback.id} for hook ${callback.hook}`); - - return (await this.runOne(callback, item, constant)) ?? item; - }; - - const identity = (item: TItem): Promise => Promise.resolve(item); - - const pipe = - (curr: (item: unknown, constant?: unknown) => Promise, next: (item: unknown, constant?: unknown) => Promise) => - async (item: unknown, constant?: unknown): Promise => - next(await curr(item, constant), constant); - - const fn = callbacks.map(wrapCallback).reduce(pipe, identity); - - return async (item: unknown, constant?: unknown): Promise => { - 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( - hook: THook, - callback: EventLikeCallbackSignatures[THook], - priority?: CallbackPriority, - id?: string, - ): void; - - add( - hook: THook, - callback: ChainedCallbackSignatures[THook], - priority?: CallbackPriority, - id?: string, - ): void; - - add( - 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(hook: THook, ...args: Parameters): void; - - run( - hook: THook, - ...args: Parameters - ): Promise>; - - run(hook: Hook, item: TItem, constant?: TConstant): Promise; - - /** - * 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 { - const runner = this.sequentialRunners.get(hook) ?? (async (item: unknown, _constant?: unknown): Promise => item); - return runner(item, constant); - } - - runAsync(hook: THook, ...args: Parameters): 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(hook: string): Cb { - 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 = { - add: (callback: (item: I, constant?: C) => R | undefined, priority?: CallbackPriority, id?: string) => void; - remove: (id: string) => void; - run: (item: I, constant?: C) => Promise; -}; -/** - * 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 +>(); diff --git a/apps/meteor/lib/callbacks/afterLeaveRoomCallback.ts b/apps/meteor/lib/callbacks/afterLeaveRoomCallback.ts new file mode 100644 index 00000000000..71a8f8c04a2 --- /dev/null +++ b/apps/meteor/lib/callbacks/afterLeaveRoomCallback.ts @@ -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'); diff --git a/apps/meteor/lib/callbacks/afterLogoutCleanUpCallback.ts b/apps/meteor/lib/callbacks/afterLogoutCleanUpCallback.ts new file mode 100644 index 00000000000..14957700e8f --- /dev/null +++ b/apps/meteor/lib/callbacks/afterLogoutCleanUpCallback.ts @@ -0,0 +1,3 @@ +import { Callbacks } from './callbacksBase'; + +export const afterLogoutCleanUpCallback = Callbacks.create('afterLogoutCleanUp'); diff --git a/apps/meteor/lib/callbacks/afterRemoveFromRoomCallback.ts b/apps/meteor/lib/callbacks/afterRemoveFromRoomCallback.ts new file mode 100644 index 00000000000..0a62bdb217c --- /dev/null +++ b/apps/meteor/lib/callbacks/afterRemoveFromRoomCallback.ts @@ -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'); diff --git a/apps/meteor/lib/callbacks/beforeCreateRoomCallback.ts b/apps/meteor/lib/callbacks/beforeCreateRoomCallback.ts new file mode 100644 index 00000000000..dd0049927a2 --- /dev/null +++ b/apps/meteor/lib/callbacks/beforeCreateRoomCallback.ts @@ -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'); diff --git a/apps/meteor/lib/callbacks/beforeCreateUserCallback.ts b/apps/meteor/lib/callbacks/beforeCreateUserCallback.ts new file mode 100644 index 00000000000..e8e8ddaef8a --- /dev/null +++ b/apps/meteor/lib/callbacks/beforeCreateUserCallback.ts @@ -0,0 +1,3 @@ +import { Callbacks } from './callbacksBase'; + +export const beforeCreateUserCallback = Callbacks.create('beforeCreateUser'); diff --git a/apps/meteor/lib/callbacks/beforeLeaveRoomCallback.ts b/apps/meteor/lib/callbacks/beforeLeaveRoomCallback.ts new file mode 100644 index 00000000000..3ade5a8018e --- /dev/null +++ b/apps/meteor/lib/callbacks/beforeLeaveRoomCallback.ts @@ -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'); diff --git a/apps/meteor/lib/callbacks/callbacksBase.ts b/apps/meteor/lib/callbacks/callbacksBase.ts new file mode 100644 index 00000000000..c08342c18d1 --- /dev/null +++ b/apps/meteor/lib/callbacks/callbacksBase.ts @@ -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 = { + (item: unknown, constant?: unknown): Promise; + hook: H; + id: string; + priority: CallbackPriority; + stack: string; +}; + +type CallbackTracker = (callback: Callback) => () => void; + +type HookTracker = (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 | undefined = undefined; + + private trackHook: HookTracker | undefined = undefined; + + private callbacks = new Map[]>(); + + private sequentialRunners = new Map Promise>(); + + private asyncRunners = new Map 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 { + const stopTracking = this.trackCallback?.(callback); + + return Promise.resolve(callback(item, constant)).finally(stopTracking); + } + + private createSequentialRunner(hook: THook, callbacks: Callback[]): (item: unknown, constant?: unknown) => Promise { + const wrapCallback = + (callback: Callback) => + async (item: unknown, constant?: unknown): Promise => { + this.logger?.debug(`Executing callback with id ${callback.id} for hook ${callback.hook}`); + + return (await this.runOne(callback, item, constant)) ?? item; + }; + + const identity = (item: TItem): Promise => Promise.resolve(item); + + const pipe = + (curr: (item: unknown, constant?: unknown) => Promise, next: (item: unknown, constant?: unknown) => Promise) => + async (item: unknown, constant?: unknown): Promise => + next(await curr(item, constant), constant); + + const fn = callbacks.map(wrapCallback).reduce(pipe, identity); + + return async (item: unknown, constant?: unknown): Promise => { + const stopTracking = this.trackHook?.({ hook, length: callbacks.length }); + + return fn(item, constant).finally(() => stopTracking?.()); + }; + } + + private createAsyncRunner(_: THook, 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: THook): Callback[] { + return this.callbacks.get(hook) ?? []; + } + + setCallbacks(hook: THook, 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( + hook: Hook, + callback: TEventLikeCallbackSignatures[Hook], + priority?: CallbackPriority, + id?: string, + ): void; + + add( + hook: Hook, + callback: TChainedCallbackSignatures[Hook], + priority?: CallbackPriority, + id?: string, + ): void; + + add( + 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, { + 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: THook, id: string): void { + const hooks = this.getCallbacks(hook).filter((callback) => callback.id !== id); + this.setCallbacks(hook, hooks); + } + + run(hook: Hook, ...args: Parameters): void; + + run( + hook: Hook, + ...args: Parameters + ): Promise>; + + run(hook: THook, item: TItem, constant?: TConstant): Promise; + + /** + * 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 { + const runner = this.sequentialRunners.get(hook) ?? (async (item: unknown, _constant?: unknown): Promise => item); + return runner(item, constant); + } + + runAsync(hook: Hook, ...args: Parameters): 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 any | Promise>( + hook: string, + ): Cb[0], ReturnType, Parameters[1]>; + + static create(hook: string): Cb { + 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 = { + add: ( + callback: (item: I, constant: C) => Promise | R | undefined | void, + priority?: CallbackPriority, + id?: string, + ) => void; + remove: (id: string) => void; + run: (item: I, constant?: C) => Promise; +}; diff --git a/apps/meteor/server/methods/logoutCleanUp.ts b/apps/meteor/server/methods/logoutCleanUp.ts index 84da0ec71a2..273887c69f6 100644 --- a/apps/meteor/server/methods/logoutCleanUp.ts +++ b/apps/meteor/server/methods/logoutCleanUp.ts @@ -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({ check(user, Object); setImmediate(() => { - void callbacks.run('afterLogoutCleanUp', user); + void afterLogoutCleanUpCallback.run(user); }); // App IPostUserLogout event hook diff --git a/apps/meteor/server/methods/removeUserFromRoom.ts b/apps/meteor/server/methods/removeUserFromRoom.ts index 63de0556f6b..5cc414843b6 100644 --- a/apps/meteor/server/methods/removeUserFromRoom.ts +++ b/apps/meteor/server/methods/removeUserFromRoom.ts @@ -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; diff --git a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts index 620c227fe11..2c0991e20ff 100644 --- a/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts +++ b/apps/meteor/server/services/federation/infrastructure/rocket-chat/hooks/index.ts @@ -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 { - callbacks.add( - 'afterLeaveRoom', - async (user: IUser, room: IRoom | undefined): Promise => { + afterLeaveRoomCallback.add( + async (user: IUser, room?: IRoom): Promise => { 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 { - callbacks.add( - 'afterRemoveFromRoom', - async (params: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom | undefined): Promise => { + afterRemoveFromRoomCallback.add( + async (params, room): Promise => { 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'); diff --git a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts index 52c8461f1a9..255af1029b1 100644 --- a/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts +++ b/apps/meteor/tests/unit/server/federation/infrastructure/rocket-chat/hooks/hooks.spec.ts @@ -9,6 +9,8 @@ const get = sinon.stub(); const hooks: Record = {}; 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); }); }); });