diff --git a/apps/meteor/app/api/server/lib/eraseTeam.spec.ts b/apps/meteor/app/api/server/lib/eraseTeam.spec.ts index 041f5b90e2b2c..8e5da6b4b59c8 100644 --- a/apps/meteor/app/api/server/lib/eraseTeam.spec.ts +++ b/apps/meteor/app/api/server/lib/eraseTeam.spec.ts @@ -40,12 +40,10 @@ describe('eraseTeam (TypeScript) module', () => { IPostRoomDeleted: 'IPostRoomDeleted', }, Apps: { - self: { isLoaded: () => false }, - getBridges: () => ({ - getListenerBridge: () => ({ - roomEvent: sandbox.stub().resolves(false), - }), - }), + self: { + isLoaded: () => false, + triggerEvent: sandbox.stub().resolves(false), + }, }, }, '@rocket.chat/models': { @@ -194,8 +192,10 @@ describe('eraseTeam (TypeScript) module', () => { const AppsStub = { AppEvents: stubs['@rocket.chat/apps'].AppEvents, Apps: { - self: { isLoaded: () => true }, - getBridges: () => ({ getListenerBridge: () => ({ roomEvent: listenerStub }) }), + self: { + isLoaded: () => true, + triggerEvent: listenerStub, + }, }, }; @@ -244,8 +244,10 @@ describe('eraseTeam (TypeScript) module', () => { const AppsStub = { AppEvents: stubs['@rocket.chat/apps'].AppEvents, Apps: { - self: { isLoaded: () => true }, - getBridges: () => ({ getListenerBridge: () => ({ roomEvent: roomEventStub }) }), + self: { + isLoaded: () => true, + triggerEvent: roomEventStub, + }, }, }; diff --git a/apps/meteor/app/api/server/lib/eraseTeam.ts b/apps/meteor/app/api/server/lib/eraseTeam.ts index 65b289ab9982b..5fd47f782539a 100644 --- a/apps/meteor/app/api/server/lib/eraseTeam.ts +++ b/apps/meteor/app/api/server/lib/eraseTeam.ts @@ -75,7 +75,7 @@ export async function eraseRoomLooseValidation(rid: string): Promise { } if (Apps.self?.isLoaded()) { - const prevent = await Apps.getBridges()?.getListenerBridge().roomEvent(AppEvents.IPreRoomDeletePrevent, room); + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomDeletePrevent, room); if (prevent) { return false; } @@ -89,7 +89,7 @@ export async function eraseRoomLooseValidation(rid: string): Promise { } if (Apps.self?.isLoaded()) { - void Apps.getBridges()?.getListenerBridge().roomEvent(AppEvents.IPostRoomDeleted, room); + void Apps.self?.triggerEvent(AppEvents.IPostRoomDeleted, room); } return true; diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js deleted file mode 100644 index 31aa2c0052695..0000000000000 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ /dev/null @@ -1,248 +0,0 @@ -import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; -import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; - -export class AppListenerBridge { - constructor(orch) { - this.orch = orch; - } - - async handleEvent(event, ...payload) { - // eslint-disable-next-line complexity - const method = (() => { - switch (event) { - case AppInterface.IPostSystemMessageSent: - case AppInterface.IPreMessageSentPrevent: - case AppInterface.IPreMessageSentExtend: - case AppInterface.IPreMessageSentModify: - case AppInterface.IPostMessageSent: - case AppInterface.IPreMessageDeletePrevent: - case AppInterface.IPostMessageDeleted: - case AppInterface.IPreMessageUpdatedPrevent: - case AppInterface.IPreMessageUpdatedExtend: - case AppInterface.IPreMessageUpdatedModify: - case AppInterface.IPostMessageUpdated: - case AppInterface.IPostMessageReacted: - case AppInterface.IPostMessageFollowed: - case AppInterface.IPostMessagePinned: - case AppInterface.IPostMessageStarred: - case AppInterface.IPostMessageReported: - return 'messageEvent'; - case AppInterface.IPreRoomCreatePrevent: - case AppInterface.IPreRoomCreateExtend: - case AppInterface.IPreRoomCreateModify: - case AppInterface.IPostRoomCreate: - case AppInterface.IPreRoomDeletePrevent: - case AppInterface.IPostRoomDeleted: - case AppInterface.IPreRoomUserJoined: - case AppInterface.IPostRoomUserJoined: - case AppInterface.IPreRoomUserLeave: - case AppInterface.IPostRoomUserLeave: - return 'roomEvent'; - /** - * @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event - */ - case AppInterface.ILivechatRoomClosedHandler: - case AppInterface.IPreLivechatRoomCreatePrevent: - case AppInterface.IPostLivechatRoomStarted: - case AppInterface.IPostLivechatRoomClosed: - case AppInterface.IPostLivechatAgentAssigned: - case AppInterface.IPostLivechatAgentUnassigned: - case AppInterface.IPostLivechatRoomTransferred: - case AppInterface.IPostLivechatGuestSaved: - case AppInterface.IPostLivechatRoomSaved: - case AppInterface.IPostLivechatDepartmentRemoved: - case AppInterface.IPostLivechatDepartmentDisabled: - return 'livechatEvent'; - case AppInterface.IPostUserCreated: - case AppInterface.IPostUserUpdated: - case AppInterface.IPostUserDeleted: - case AppInterface.IPostUserLogin: - case AppInterface.IPostUserLogout: - case AppInterface.IPostUserStatusChanged: - return 'userEvent'; - default: - return 'defaultEvent'; - } - })(); - - return this[method](event, ...payload); - } - - async defaultEvent(inte, payload) { - return this.orch.getManager().getListenerManager().executeListener(inte, payload); - } - - async messageEvent(inte, message, ...payload) { - const msg = await this.orch.getConverters().get('messages').convertMessage(message); - - const params = (() => { - switch (inte) { - case AppInterface.IPostMessageDeleted: - const [userDeleted] = payload; - return { - message: msg, - user: this.orch.getConverters().get('users').convertToApp(userDeleted), - }; - case AppInterface.IPostMessageReacted: - const [userReacted, reaction, isReacted] = payload; - return { - message: msg, - user: this.orch.getConverters().get('users').convertToApp(userReacted), - reaction, - isReacted, - }; - case AppInterface.IPostMessageFollowed: - const [userFollowed, isUnfollow] = payload; - return { - message: msg, - user: this.orch.getConverters().get('users').convertToApp(userFollowed), - isUnfollow, - }; - case AppInterface.IPostMessagePinned: - const [userPinned, isUnpinned] = payload; - return { - message: msg, - user: this.orch.getConverters().get('users').convertToApp(userPinned), - isUnpinned, - }; - case AppInterface.IPostMessageStarred: - const [userStarred, isStarred] = payload; - return { - message: msg, - user: this.orch.getConverters().get('users').convertToApp(userStarred), - isStarred, - }; - case AppInterface.IPostMessageReported: - const [userReported, reason] = payload; - return { - message: msg, - user: this.orch.getConverters().get('users').convertToApp(userReported), - reason, - }; - default: - return msg; - } - })(); - - const result = await this.orch.getManager().getListenerManager().executeListener(inte, params); - - if (typeof result === 'boolean') { - return result; - } - return this.orch.getConverters().get('messages').convertAppMessage(result); - } - - async roomEvent(inte, room, ...payload) { - const rm = await this.orch.getConverters().get('rooms').convertRoom(room); - - const params = (() => { - switch (inte) { - case AppInterface.IPreRoomUserJoined: - case AppInterface.IPostRoomUserJoined: - const [joiningUser, invitingUser] = payload; - return { - room: rm, - joiningUser: this.orch.getConverters().get('users').convertToApp(joiningUser), - invitingUser: this.orch.getConverters().get('users').convertToApp(invitingUser), - }; - case AppInterface.IPreRoomUserLeave: - case AppInterface.IPostRoomUserLeave: - const [leavingUser, removedBy] = payload; - return { - room: rm, - leavingUser: this.orch.getConverters().get('users').convertToApp(leavingUser), - removedBy: this.orch.getConverters().get('users').convertToApp(removedBy), - }; - default: - return rm; - } - })(); - - const result = await this.orch.getManager().getListenerManager().executeListener(inte, params); - - if (typeof result === 'boolean') { - return result; - } - return this.orch.getConverters().get('rooms').convertAppRoom(result); - } - - async livechatEvent(inte, data) { - switch (inte) { - case AppInterface.IPostLivechatAgentAssigned: - case AppInterface.IPostLivechatAgentUnassigned: - return this.orch - .getManager() - .getListenerManager() - .executeListener(inte, { - room: await this.orch.getConverters().get('rooms').convertRoom(data.room), - agent: this.orch.getConverters().get('users').convertToApp(data.user), - }); - case AppInterface.IPostLivechatRoomTransferred: - const converter = data.type === LivechatTransferEventType.AGENT ? 'users' : 'departments'; - - return this.orch - .getManager() - .getListenerManager() - .executeListener(inte, { - type: data.type, - room: await this.orch.getConverters().get('rooms').convertById(data.room), - from: await this.orch.getConverters().get(converter).convertById(data.from), - to: await this.orch.getConverters().get(converter).convertById(data.to), - }); - case AppInterface.IPostLivechatGuestSaved: - return this.orch - .getManager() - .getListenerManager() - .executeListener(inte, await this.orch.getConverters().get('visitors').convertById(data)); - case AppInterface.IPostLivechatRoomSaved: - return this.orch - .getManager() - .getListenerManager() - .executeListener(inte, await this.orch.getConverters().get('rooms').convertById(data)); - case AppInterface.IPostLivechatDepartmentDisabled: - return this.orch - .getManager() - .getListenerManager() - .executeListener(inte, await this.orch.getConverters().get('departments').convertDepartment(data)); - case AppInterface.IPostLivechatDepartmentRemoved: - return this.orch - .getManager() - .getListenerManager() - .executeListener(inte, await this.orch.getConverters().get('departments').convertDepartment(data)); - default: - const room = await this.orch.getConverters().get('rooms').convertRoom(data); - - return this.orch.getManager().getListenerManager().executeListener(inte, room); - } - } - - async userEvent(inte, data) { - let context; - switch (inte) { - case AppInterface.IPostUserLoggedIn: - case AppInterface.IPostUserLogout: - context = this.orch.getConverters().get('users').convertToApp(data.user); - return this.orch.getManager().getListenerManager().executeListener(inte, context); - case AppInterface.IPostUserStatusChanged: - const { currentStatus, previousStatus } = data; - context = { - user: this.orch.getConverters().get('users').convertToApp(data.user), - currentStatus, - previousStatus, - }; - - return this.orch.getManager().getListenerManager().executeListener(inte, context); - case AppInterface.IPostUserCreated: - case AppInterface.IPostUserUpdated: - case AppInterface.IPostUserDeleted: - context = { - user: this.orch.getConverters().get('users').convertToApp(data.user), - performedBy: this.orch.getConverters().get('users').convertToApp(data.performedBy), - }; - if (inte === AppInterface.IPostUserUpdated) { - context.previousData = this.orch.getConverters().get('users').convertToApp(data.previousUser); - } - return this.orch.getManager().getListenerManager().executeListener(inte, context); - } - } -} diff --git a/apps/meteor/app/apps/server/bridges/listeners.ts b/apps/meteor/app/apps/server/bridges/listeners.ts new file mode 100644 index 0000000000000..6abf276d9f926 --- /dev/null +++ b/apps/meteor/app/apps/server/bridges/listeners.ts @@ -0,0 +1,508 @@ +import type { IAppServerOrchestrator, IAppsRoom, IAppsLivechatRoom, IAppsMessage } from '@rocket.chat/apps'; +import type { IPreEmailSentContext } from '@rocket.chat/apps-engine/definition/email'; +import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent'; +import { LivechatTransferEventType } from '@rocket.chat/apps-engine/definition/livechat'; +import { isLivechatRoom } from '@rocket.chat/apps-engine/definition/livechat/ILivechatRoom'; +import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; +import type { UIKitIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit'; +import type { IUIKitLivechatIncomingInteraction } from '@rocket.chat/apps-engine/definition/uikit/livechat'; +import type { IFileUploadContext } from '@rocket.chat/apps-engine/definition/uploads'; +import type { IUserContext, IUserUpdateContext } from '@rocket.chat/apps-engine/definition/users'; +import type { IMessage, IRoom, IUser, ILivechatDepartment } from '@rocket.chat/core-typings'; + +type LivechatTransferData = { + type: LivechatTransferEventType; + room: string; + from: string; + to: string; +}; + +type LivechatAgentData = { + room: IRoom; + user: IUser; +}; + +type UserStatusChangedData = { + user: IUser; + currentStatus: string; + previousStatus: string; +}; + +type UserCrudData = { + user: IUser; + performedBy?: IUser; + previousUser?: IUser; +}; + +// IPostMessageSentToBot is an internally triggered event, based on IPostMessageSent +// so we don't add it here +type HandleMessageEvent = + | { + event: AppInterface.IPostMessageDeleted; + payload: [IMessage, IUser]; + } + | { + event: AppInterface.IPostMessageReacted; + payload: [IMessage, IUser, string, boolean]; + } + | { + event: AppInterface.IPostMessageFollowed; + payload: [IMessage, IUser, boolean]; + } + | { + event: AppInterface.IPostMessagePinned; + payload: [IMessage, IUser, boolean]; + } + | { + event: AppInterface.IPostMessageStarred; + payload: [IMessage, IUser, boolean]; + } + | { + event: AppInterface.IPostMessageReported; + payload: [IMessage, IUser, string]; + } + | { + event: + | AppInterface.IPostSystemMessageSent + | AppInterface.IPreMessageSentPrevent + | AppInterface.IPreMessageSentExtend + | AppInterface.IPreMessageSentModify + | AppInterface.IPostMessageSent + | AppInterface.IPreMessageDeletePrevent + | AppInterface.IPreMessageUpdatedPrevent + | AppInterface.IPreMessageUpdatedExtend + | AppInterface.IPreMessageUpdatedModify + | AppInterface.IPostMessageUpdated; + payload: [IMessage]; + }; + +type HandleRoomEvent = + | { + event: AppInterface.IPreRoomUserJoined | AppInterface.IPostRoomUserJoined; + payload: [IRoom, IUser, IUser]; + } + | { + event: AppInterface.IPreRoomUserLeave | AppInterface.IPostRoomUserLeave; + payload: [IRoom, IUser, IUser]; + } + | { + event: + | AppInterface.IPreRoomCreatePrevent + | AppInterface.IPreRoomCreateExtend + | AppInterface.IPreRoomCreateModify + | AppInterface.IPostRoomCreate + | AppInterface.IPreRoomDeletePrevent + | AppInterface.IPostRoomDeleted + | AppInterface.IPreRoomUserJoined + | AppInterface.IPostRoomUserJoined + | AppInterface.IPreRoomUserLeave + | AppInterface.IPostRoomUserLeave; + payload: [IRoom]; + }; + +type HandleLivechatEvent = + | { + event: AppInterface.IPostLivechatAgentAssigned | AppInterface.IPostLivechatAgentUnassigned; + payload: [LivechatAgentData]; + } + | { + event: AppInterface.IPostLivechatRoomTransferred; + payload: [LivechatTransferData]; + } + | { + event: AppInterface.IPostLivechatGuestSaved; + payload: [string]; + } + | { + event: AppInterface.IPostLivechatRoomSaved; + payload: [string]; + } + | { + event: AppInterface.IPostLivechatDepartmentRemoved; + payload: [ILivechatDepartment]; + } + | { + event: AppInterface.IPostLivechatDepartmentDisabled; + payload: [ILivechatDepartment]; + } + | { + event: + | AppInterface.ILivechatRoomClosedHandler + | AppInterface.IPreLivechatRoomCreatePrevent + | AppInterface.IPostLivechatRoomStarted + | AppInterface.IPostLivechatRoomClosed; + payload: [IRoom]; + }; + +type HandleUserEvent = + | { + event: AppInterface.IPostUserLoggedIn | AppInterface.IPostUserLoggedOut; + payload: [IUser]; + } + | { + event: AppInterface.IPostUserStatusChanged; + payload: [UserStatusChangedData]; + } + | { + event: AppInterface.IPostUserDeleted | AppInterface.IPostUserCreated | AppInterface.IPostUserUpdated; + payload: [UserCrudData]; + }; + +type HandleDefaultEvent = + | { + event: AppInterface.IPostExternalComponentOpened | AppInterface.IPostExternalComponentClosed; + payload: [IExternalComponent]; + } + | { + event: AppInterface.IUIKitInteractionHandler; + payload: [UIKitIncomingInteraction]; + } + | { + event: AppInterface.IUIKitLivechatInteractionHandler; + payload: [IUIKitLivechatIncomingInteraction]; + } + | { + event: AppInterface.IPreFileUpload; + payload: [IFileUploadContext]; + } + | { + event: AppInterface.IPreEmailSent; + payload: [IPreEmailSentContext]; + }; + +type HandleEvent = HandleMessageEvent | HandleRoomEvent | HandleLivechatEvent | HandleUserEvent | HandleDefaultEvent; + +export class AppListenerBridge { + constructor(private readonly orch: IAppServerOrchestrator) {} + + // eslint-disable-next-line complexity + async handleEvent(args: HandleEvent): Promise { + switch (args.event) { + case AppInterface.IPostMessageDeleted: + case AppInterface.IPostMessageReacted: + case AppInterface.IPostMessageFollowed: + case AppInterface.IPostMessagePinned: + case AppInterface.IPostMessageStarred: + case AppInterface.IPostMessageReported: + case AppInterface.IPostSystemMessageSent: + case AppInterface.IPreMessageSentPrevent: + case AppInterface.IPreMessageSentExtend: + case AppInterface.IPreMessageSentModify: + case AppInterface.IPostMessageSent: + case AppInterface.IPreMessageDeletePrevent: + case AppInterface.IPreMessageUpdatedPrevent: + case AppInterface.IPreMessageUpdatedExtend: + case AppInterface.IPreMessageUpdatedModify: + case AppInterface.IPostMessageUpdated: + return this.messageEvent(args); + case AppInterface.IPreRoomCreatePrevent: + case AppInterface.IPreRoomCreateExtend: + case AppInterface.IPreRoomCreateModify: + case AppInterface.IPostRoomCreate: + case AppInterface.IPreRoomDeletePrevent: + case AppInterface.IPostRoomDeleted: + case AppInterface.IPreRoomUserJoined: + case AppInterface.IPostRoomUserJoined: + case AppInterface.IPreRoomUserLeave: + case AppInterface.IPostRoomUserLeave: + return this.roomEvent(args); + /** + * @deprecated please prefer the AppInterface.IPostLivechatRoomClosed event + */ + case AppInterface.ILivechatRoomClosedHandler: + case AppInterface.IPreLivechatRoomCreatePrevent: + case AppInterface.IPostLivechatRoomStarted: + case AppInterface.IPostLivechatRoomClosed: + case AppInterface.IPostLivechatAgentAssigned: + case AppInterface.IPostLivechatAgentUnassigned: + case AppInterface.IPostLivechatRoomTransferred: + case AppInterface.IPostLivechatGuestSaved: + case AppInterface.IPostLivechatRoomSaved: + case AppInterface.IPostLivechatDepartmentRemoved: + case AppInterface.IPostLivechatDepartmentDisabled: + return this.livechatEvent(args); + case AppInterface.IPostUserCreated: + case AppInterface.IPostUserUpdated: + case AppInterface.IPostUserDeleted: + case AppInterface.IPostUserLoggedIn: + case AppInterface.IPostUserLoggedOut: + case AppInterface.IPostUserStatusChanged: + return this.userEvent(args); + default: + return this.defaultEvent(args); + } + } + + async defaultEvent(args: HandleDefaultEvent): Promise { + return this.orch.getManager().getListenerManager().executeListener(args.event, args.payload[0]); + } + + async messageEvent(args: HandleMessageEvent): Promise { + const [message] = args.payload; + + const msg = await this.orch.getConverters().get('messages').convertMessage(message); + + const result = await (() => { + switch (args.event) { + case AppInterface.IPostMessageDeleted: + const [, userDeleted] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userDeleted), + }); + case AppInterface.IPostMessageReacted: + const [, userReacted, reaction, isReacted] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userReacted), + reaction, + isReacted, + }); + case AppInterface.IPostMessageFollowed: + const [, userFollowed, isFollowed] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userFollowed), + isFollowed, + }); + case AppInterface.IPostMessagePinned: + const [, userPinned, isPinned] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userPinned), + isPinned, + }); + case AppInterface.IPostMessageStarred: + const [, userStarred, isStarred] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userStarred), + isStarred, + }); + case AppInterface.IPostMessageReported: + const [, userReported, reason] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + message: msg, + user: this.orch.getConverters().get('users').convertToApp(userReported), + reason, + }); + default: + return this.orch.getManager().getListenerManager().executeListener(args.event, msg); + } + })(); + + // TODO: weird that boolean is not returned by executeListener + if (typeof result === 'boolean' || result === undefined) { + return result ?? undefined; + } + + return this.orch + .getConverters() + .get('messages') + .convertAppMessage(result as IAppsMessage); + } + + async roomEvent(args: HandleRoomEvent): Promise { + const [room] = args.payload; + + const rm = await this.orch.getConverters().get('rooms').convertRoom(room); + + const result = await (() => { + switch (args.event) { + case AppInterface.IPreRoomUserJoined: + case AppInterface.IPostRoomUserJoined: + const [, joiningUser, invitingUser] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + room: rm, + joiningUser: this.orch.getConverters().get('users').convertToApp(joiningUser)!, + inviter: this.orch.getConverters().get('users').convertToApp(invitingUser), + }); + case AppInterface.IPreRoomUserLeave: + case AppInterface.IPostRoomUserLeave: + const [, leavingUser, removedBy] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + room: rm, + leavingUser: this.orch.getConverters().get('users').convertToApp(leavingUser)!, + removedBy: this.orch.getConverters().get('users').convertToApp(removedBy), + }); + default: + return this.orch.getManager().getListenerManager().executeListener(args.event, rm); + } + })(); + + if (typeof result === 'boolean' || result === undefined) { + return result ?? undefined; + } + + return this.orch.getConverters().get('rooms').convertAppRoom(result); + } + + async livechatEvent(args: HandleLivechatEvent): Promise { + switch (args.event) { + case AppInterface.IPostLivechatAgentAssigned: + case AppInterface.IPostLivechatAgentUnassigned: + const [agentData] = args.payload; + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + room: (await this.orch.getConverters().get('rooms').convertRoom(agentData.room)) as IAppsLivechatRoom, + agent: this.orch.getConverters().get('users').convertToApp(agentData.user), + }); + + case AppInterface.IPostLivechatRoomTransferred: { + const [transferData] = args.payload; + const converter = transferData.type === LivechatTransferEventType.AGENT ? 'users' : 'departments'; + + const room = await this.orch.getConverters().get('rooms').convertById(transferData.room); + const from = await this.orch.getConverters().get(converter).convertById(transferData.from); + const to = await this.orch.getConverters().get(converter).convertById(transferData.to); + + if (!room) { + throw new Error(`Room with id ${transferData.room} not found`); + } + + if (!to) { + throw new Error(`Transfer to entity with id ${transferData.to} not found`); + } + + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, { + room, + from: from as NonNullable, // type definition in the apps-engine seems to be incorrect + to, + type: transferData.type, + }); + } + + case AppInterface.IPostLivechatGuestSaved: { + const [visitorId] = args.payload; + const visitor = await this.orch.getConverters().get('visitors').convertById(visitorId); + + if (!visitor) { + throw new Error(`Visitor with id ${visitorId} not found`); + } + + return this.orch.getManager().getListenerManager().executeListener(args.event, visitor); + } + + case AppInterface.IPostLivechatRoomSaved: { + const [roomId] = args.payload; + const room = await this.orch.getConverters().get('rooms').convertById(roomId); + + if (!room) { + throw new Error(`Room with id ${roomId} not found`); + } + + return this.orch + .getManager() + .getListenerManager() + .executeListener(args.event, room as IAppsLivechatRoom); + } + + case AppInterface.IPostLivechatDepartmentDisabled: { + const [departmentData] = args.payload; + const department = await this.orch.getConverters().get('departments').convertDepartment(departmentData); + + if (!department) { + throw new Error(`Department ${departmentData._id} not found`); + } + + return this.orch.getManager().getListenerManager().executeListener(args.event, { department }); + } + + case AppInterface.IPostLivechatDepartmentRemoved: { + const [departmentData] = args.payload; + const department = await this.orch.getConverters().get('departments').convertDepartment(departmentData); + + if (!department) { + throw new Error(`Department ${departmentData._id} not found`); + } + + return this.orch.getManager().getListenerManager().executeListener(args.event, { department }); + } + + default: + const [roomData] = args.payload; + const room = await this.orch.getConverters().get('rooms').convertRoom(roomData); + + if (!room) { + throw new Error(`Room ${roomData._id} not found`); + } + + if (!isLivechatRoom(room)) { + throw new Error(`Room ${roomData._id} is not a livechat room`); + } + + return this.orch.getManager().getListenerManager().executeListener(args.event, room); + } + } + + async userEvent(args: HandleUserEvent): Promise { + switch (args.event) { + case AppInterface.IPostUserLoggedIn: + case AppInterface.IPostUserLoggedOut: { + const [loggedInUser] = args.payload; + const context = this.orch.getConverters().get('users').convertToApp(loggedInUser); + return this.orch.getManager().getListenerManager().executeListener(args.event, context); + } + case AppInterface.IPostUserStatusChanged: { + const [statusData] = args.payload; + const { currentStatus, previousStatus } = statusData; + const context = { + user: this.orch.getConverters().get('users').convertToApp(statusData.user), + currentStatus, + previousStatus, + }; + + return this.orch.getManager().getListenerManager().executeListener(args.event, context); + } + case AppInterface.IPostUserCreated: + case AppInterface.IPostUserDeleted: { + const [crudData] = args.payload; + const context: IUserContext = { + user: this.orch.getConverters().get('users').convertToApp(crudData.user), + performedBy: this.orch.getConverters().get('users').convertToApp(crudData.performedBy), + }; + + return this.orch.getManager().getListenerManager().executeListener(args.event, context); + } + case AppInterface.IPostUserUpdated: { + const [crudData] = args.payload; + const context: IUserUpdateContext = { + user: this.orch.getConverters().get('users').convertToApp(crudData.user), + performedBy: this.orch.getConverters().get('users').convertToApp(crudData.performedBy), + previousData: this.orch.getConverters().get('users').convertToApp(crudData.previousUser), + }; + + return this.orch.getManager().getListenerManager().executeListener(args.event, context); + } + } + } +} diff --git a/apps/meteor/app/lib/server/functions/deleteMessage.ts b/apps/meteor/app/lib/server/functions/deleteMessage.ts index 1c3fb32ef28bd..09e7e631a8c41 100644 --- a/apps/meteor/app/lib/server/functions/deleteMessage.ts +++ b/apps/meteor/app/lib/server/functions/deleteMessage.ts @@ -33,16 +33,13 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise 0; const keepHistory = settings.get('Message_KeepHistory') || isThread; const showDeletedStatus = settings.get('Message_ShowDeletedStatus') || isThread; - const bridges = Apps.self?.isLoaded() && Apps.getBridges(); const room = await Rooms.findOneById(message.rid, { projection: { lastMessage: 1, prid: 1, mid: 1, federated: 1, federation: 1 } }); if (deletedMsg) { - if (bridges) { - const prevent = await bridges.getListenerBridge().messageEvent(AppEvents.IPreMessageDeletePrevent, deletedMsg); - if (prevent) { - throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.'); - } + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreMessageDeletePrevent, deletedMsg); + if (prevent) { + throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the message deleting.'); } if (room) { @@ -103,8 +100,8 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise { diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index ff91d04819846..7ea586f5ce38a 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -291,7 +291,7 @@ export const RoutingManager: Routing = { throw new Error('error-room-not-found'); } - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatAgentAssigned, { room: roomAfterUpdate, user }); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatAgentAssigned, { room: roomAfterUpdate, user }); void afterTakeInquiry({ inquiry: returnedInquiry, room: roomAfterUpdate, agent }); void notifyOnLivechatInquiryChangedById(inquiry._id, 'updated', { diff --git a/apps/meteor/app/livechat/server/lib/closeRoom.ts b/apps/meteor/app/livechat/server/lib/closeRoom.ts index 24770e40af29c..0c96adc42ad79 100644 --- a/apps/meteor/app/livechat/server/lib/closeRoom.ts +++ b/apps/meteor/app/livechat/server/lib/closeRoom.ts @@ -95,8 +95,8 @@ async function afterRoomClosed( * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed * in the next major version of the Apps-Engine */ - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, newRoom); + void Apps.self?.triggerEvent(AppEvents.ILivechatRoomClosedHandler, newRoom); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatRoomClosed, newRoom); }); const visitor = isRoomClosedByVisitorParams(params) ? params.visitor : undefined; diff --git a/apps/meteor/app/livechat/server/lib/departmentsLib.ts b/apps/meteor/app/livechat/server/lib/departmentsLib.ts index d6a8acc646315..9011704905e9e 100644 --- a/apps/meteor/app/livechat/server/lib/departmentsLib.ts +++ b/apps/meteor/app/livechat/server/lib/departmentsLib.ts @@ -139,10 +139,7 @@ export async function saveDepartment( // Disable event if (department?.enabled && !departmentDB?.enabled) { await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); - void Apps.self - ?.getBridges() - ?.getListenerBridge() - .livechatEvent(AppEvents.IPostLivechatDepartmentDisabled, { department: departmentDB }); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatDepartmentDisabled, { department: departmentDB }); } if (departmentUnit) { @@ -272,7 +269,7 @@ export async function removeDepartment(departmentId: string) { } await callbacks.run('livechat.afterRemoveDepartment', { department, agentsIds: removedAgents.map(({ agentId }) => agentId) }); - void Apps.self?.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatDepartmentRemoved, { department }); + void Apps.self?.triggerEvent(AppEvents.IPostLivechatDepartmentRemoved, { department }); return ret; } diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index 7d490406d007f..e5e851ef8f2b1 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -1,3 +1,4 @@ +import { AppEvents, type IAppServerOrchestrator } from '@rocket.chat/apps'; import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { UiKitCoreApp } from '@rocket.chat/core-services'; import type { OperationParams, UrlParams } from '@rocket.chat/rest-typings'; @@ -11,7 +12,6 @@ import { WebApp } from 'meteor/webapp'; import { authenticationMiddleware } from '../../../../app/api/server/middlewares/authentication'; import { settings } from '../../../../app/settings/server'; -import type { AppServerOrchestrator } from '../orchestrator'; import { Apps } from '../orchestrator'; const apiServer = express(); @@ -193,9 +193,9 @@ router.post('/:id', async (req: UiKitUserInteractionRequest, res, next) => { }); export class AppUIKitInteractionApi { - orch: AppServerOrchestrator; + orch: IAppServerOrchestrator; - constructor(orch: AppServerOrchestrator) { + constructor(orch: IAppServerOrchestrator) { this.orch = orch; router.post('/:id', this.routeHandler); @@ -212,9 +212,9 @@ export class AppUIKitInteractionApi { const rid = 'rid' in req.body ? req.body.rid : undefined; const { visitor } = req.body; - const room = await orch.getConverters()?.get('rooms').convertById(rid); const user = orch.getConverters()?.get('users').convertToApp(req.user); - const message = mid && (await orch.getConverters()?.get('messages').convertById(mid)); + const message = mid ? await orch.getConverters()?.get('messages').convertById(mid) : undefined; + const room = rid ? await orch.getConverters()?.get('rooms').convertById(rid) : undefined; const action = { type, @@ -230,7 +230,7 @@ export class AppUIKitInteractionApi { }; try { - const eventInterface = !visitor ? 'IUIKitInteractionHandler' : 'IUIKitLivechatInteractionHandler'; + const eventInterface = !visitor ? AppEvents.IUIKitInteractionHandler : AppEvents.IUIKitLivechatInteractionHandler; const result = await orch.triggerEvent(eventInterface, action); @@ -261,7 +261,7 @@ export class AppUIKitInteractionApi { }; try { - const result = await orch.triggerEvent('IUIKitInteractionHandler', action); + const result = await orch.triggerEvent(AppEvents.IUIKitInteractionHandler, action); res.send(result); } catch (e) { @@ -286,7 +286,7 @@ export class AppUIKitInteractionApi { }; try { - const result = await orch.triggerEvent('IUIKitInteractionHandler', action); + const result = await orch.triggerEvent(AppEvents.IUIKitInteractionHandler, action); res.send(result); } catch (e) { @@ -307,9 +307,9 @@ export class AppUIKitInteractionApi { payload: { context, message: msgText }, } = req.body; - const room = await orch.getConverters()?.get('rooms').convertById(rid); const user = orch.getConverters()?.get('users').convertToApp(req.user); - const message = mid && (await orch.getConverters()?.get('messages').convertById(mid)); + const room = rid ? await orch.getConverters()?.get('rooms').convertById(rid) : undefined; + const message = mid ? await orch.getConverters()?.get('messages').convertById(mid) : undefined; const action = { type, @@ -327,7 +327,7 @@ export class AppUIKitInteractionApi { }; try { - const result = await orch.triggerEvent('IUIKitInteractionHandler', action); + const result = await orch.triggerEvent(AppEvents.IUIKitInteractionHandler, action); res.send(result); } catch (e) { diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index 743b27639f050..c8bcb1cfed5b9 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -344,7 +344,7 @@ export class AppServerOrchestrator { return this.getBridges() .getListenerBridge() - .handleEvent(event, ...payload) + .handleEvent({ event, payload }) .catch((error) => { if (error instanceof EssentialAppDisabledException) { throw new Meteor.Error('error-essential-app-disabled'); diff --git a/apps/meteor/server/lib/eraseRoom.ts b/apps/meteor/server/lib/eraseRoom.ts index 4b2d618643f0c..72933a1d34a82 100644 --- a/apps/meteor/server/lib/eraseRoom.ts +++ b/apps/meteor/server/lib/eraseRoom.ts @@ -41,7 +41,7 @@ export async function eraseRoom(roomOrId: string | IRoom, uid: string): Promise< } if (Apps.self?.isLoaded()) { - const prevent = await Apps.getBridges()?.getListenerBridge().roomEvent(AppEvents.IPreRoomDeletePrevent, room); + const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomDeletePrevent, room); if (prevent) { throw new Meteor.Error('error-app-prevented-deleting', 'A Rocket.Chat App prevented the room erasing.'); } @@ -57,6 +57,6 @@ export async function eraseRoom(roomOrId: string | IRoom, uid: string): Promise< } if (Apps.self?.isLoaded()) { - void Apps.getBridges()?.getListenerBridge().roomEvent(AppEvents.IPostRoomDeleted, room); + void Apps.self?.triggerEvent(AppEvents.IPostRoomDeleted, room); } } diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 669441a63c186..df64348d00a99 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -1,4 +1,4 @@ -import { Apps } from '@rocket.chat/apps'; +import { AppEvents, Apps } from '@rocket.chat/apps'; import type { IMessageService } from '@rocket.chat/core-services'; import { Authorization, ServiceClassInternal } from '@rocket.chat/core-services'; import { type IMessage, type MessageTypesValues, type IUser, type IRoom, isEditedMessage, type AtLeast } from '@rocket.chat/core-typings'; @@ -202,7 +202,7 @@ export class MessageService extends ServiceClassInternal implements IMessageServ } if (Apps.self?.isLoaded()) { - void Apps.getBridges()?.getListenerBridge().messageEvent('IPostSystemMessageSent', createdMessage); + void Apps.self?.triggerEvent(AppEvents.IPostSystemMessageSent, createdMessage); } void notifyOnMessageChange({ id: createdMessage._id, data: createdMessage }); diff --git a/packages/apps-engine/src/server/managers/AppListenerManager.ts b/packages/apps-engine/src/server/managers/AppListenerManager.ts index 273f969ce1045..e2ce55691b88b 100644 --- a/packages/apps-engine/src/server/managers/AppListenerManager.ts +++ b/packages/apps-engine/src/server/managers/AppListenerManager.ts @@ -32,7 +32,7 @@ import type { ProxiedApp } from '../ProxiedApp'; import { Utilities } from '../misc/Utilities'; import { JSONRPC_METHOD_NOT_FOUND } from '../runtime/deno/AppsEngineDenoRuntime'; -interface IListenerExecutor { +export interface IListenerExecutor { [AppInterface.IPreMessageSentPrevent]: { args: [IMessage]; result: boolean;