diff --git a/.changeset/happy-nails-fry.md b/.changeset/happy-nails-fry.md new file mode 100644 index 0000000000000..ebc17ea2dfe8b --- /dev/null +++ b/.changeset/happy-nails-fry.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/apps-engine": minor +"@rocket.chat/apps": minor +--- + +Adds a new IPostSystemMessageSent event, that is triggered whenever a new System Message is sent diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index 13db1179310c5..d18d5bdceb656 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -10,6 +10,7 @@ export class AppListenerBridge { // eslint-disable-next-line complexity const method = (() => { switch (event) { + case AppInterface.IPostSystemMessageSent: case AppInterface.IPreMessageSentPrevent: case AppInterface.IPreMessageSentExtend: case AppInterface.IPreMessageSentModify: diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 704dde1850490..530a8aed6e4a5 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -48,6 +48,7 @@ export class AppMessagesConverter { attachments: getAttachments, sender: 'u', threadMsgCount: 'tcount', + type: 't', }; return transformMappedData(message, map); @@ -91,6 +92,7 @@ export class AppMessagesConverter { groupable: 'groupable', token: 'token', blocks: 'blocks', + type: 't', room: async (message) => { const result = await cache.get('room')(message.rid); delete message.rid; diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 828de8451a217..599e75987eda4 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -284,9 +284,9 @@ export const sendMessage = async function (user: any, message: any, room: any, u } if (Apps.self?.isLoaded()) { - // This returns a promise, but it won't mutate anything about the message - // so, we don't really care if it is successful or fails - void Apps.getBridges()?.getListenerBridge().messageEvent('IPostMessageSent', message); + // If the message has a type (system message), we should notify the listener about it + const messageEvent = message.t ? 'IPostSystemMessageSent' : 'IPostMessageSent'; + void Apps.getBridges()?.getListenerBridge().messageEvent(messageEvent, message); } // TODO: is there an opportunity to send returned data to notifyOnMessageChange? diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 85afcf394f28b..828d2da19ead4 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -1,3 +1,4 @@ +import { 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'; @@ -152,6 +153,10 @@ export class MessageService extends ServiceClassInternal implements IMessageServ throw new Error('Failed to find the created message.'); } + if (Apps.self?.isLoaded()) { + void Apps.getBridges()?.getListenerBridge().messageEvent('IPostSystemMessageSent', createdMessage); + } + void notifyOnMessageChange({ id: createdMessage._id, data: createdMessage }); void notifyOnRoomChangedById(rid); diff --git a/apps/meteor/tests/unit/app/apps/server/messages.tests.js b/apps/meteor/tests/unit/app/apps/server/messages.tests.js index 04866f6440b6d..195f02bbe8930 100644 --- a/apps/meteor/tests/unit/app/apps/server/messages.tests.js +++ b/apps/meteor/tests/unit/app/apps/server/messages.tests.js @@ -101,12 +101,6 @@ describe('The AppMessagesConverter instance', () => { }); }); - it('should add an `_unmappedProperties_` field to the converted message which contains the `t` property of the message', async () => { - const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('SimpleMessageMock')); - - expect(appMessage).to.have.property('_unmappedProperties_').which.has.property('t', 'uj'); - }); - it("should return basic sender info when it's not a Rocket.Chat user (e.g. Livechat Guest)", async () => { const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('LivechatGuestMessageMock')); diff --git a/packages/apps-engine/src/definition/messages/IMessage.ts b/packages/apps-engine/src/definition/messages/IMessage.ts index d7ea6357497a8..50d36f9d5db2f 100644 --- a/packages/apps-engine/src/definition/messages/IMessage.ts +++ b/packages/apps-engine/src/definition/messages/IMessage.ts @@ -6,6 +6,7 @@ import type { IUser, IUserLookup } from '../users'; import type { IMessageAttachment } from './IMessageAttachment'; import type { IMessageFile } from './IMessageFile'; import type { IMessageReactions } from './IMessageReaction'; +import type { MessageType } from './MessageType'; export interface IMessage { id?: string; @@ -31,4 +32,5 @@ export interface IMessage { pinned?: boolean; pinnedAt?: Date; pinnedBy?: IUserLookup; + type?: MessageType; } diff --git a/packages/apps-engine/src/definition/messages/IPostSystemMessageSent.ts b/packages/apps-engine/src/definition/messages/IPostSystemMessageSent.ts new file mode 100644 index 0000000000000..053350f1a32bc --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IPostSystemMessageSent.ts @@ -0,0 +1,13 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import type { IMessage } from './IMessage'; + +/** + * Handler for when a System message is sent. + * System messages are messages that are not sent by a user, but by the system itself. + */ +export interface IPostSystemMessageSent { + /** + * Method called *after* the system message is sent to the other clients. + */ + executePostSystemMessageSent(message: IMessage, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/messages/MessageType.ts b/packages/apps-engine/src/definition/messages/MessageType.ts new file mode 100644 index 0000000000000..3afa6eb614b77 --- /dev/null +++ b/packages/apps-engine/src/definition/messages/MessageType.ts @@ -0,0 +1,143 @@ +/** + * Usually, normal messages won't have a type, but system messages will, end those are the types that are available. + */ +export type MessageType = + /** Sent when a voip call has started */ + | 'voip-call-started' + /** Sent when a voip call has been declined */ + | 'voip-call-declined' + /** Sent when a voip call is put on hold */ + | 'voip-call-on-hold' + /** Sent when a voip call is unhold */ + | 'voip-call-unhold' + /** Sent when a voip call is paused */ + | 'voip-call-ended' + /** Sent when a voip call is over, contains the duration of the call */ + | 'voip-call-duration' + /** Sent when a voip call is ended */ + | 'voip-call-wrapup' + /** Sent when a voip call is ended unexpectedly */ + | 'voip-call-ended-unexpectedly' + /** Sent when a user is removed from main room of a team */ + | 'removed-user-from-team' + /** Sent when a user is added to a team */ + | 'added-user-to-team' + /** Sent when a user leaves a team */ + | 'ult' + /** Sent when a user converts a room into a team */ + | 'user-converted-to-team' + /** Sent when a user converts a room into a channel */ + | 'user-converted-to-channel' + /** Sent when a user removes a room from a team */ + | 'user-removed-room-from-team' + /** Sent when a user deletes a room inside a team */ + | 'user-deleted-room-from-team' + /** Sent when a user adds a room to a team */ + | 'user-added-room-to-team' + /** Sent when a user joins a team */ + | 'ujt' + /** Sent when the navigation history of a livechat conversation is requested */ + | 'livechat_navigation_history' + /** Sent when the conversation is transferred */ + | 'livechat_transfer_history' + /** Sent when the transcript is requested */ + | 'livechat_transcript_history' + /** Sent when a video call is requested */ + | 'livechat_video_call' + /** Sent when there is a history fallback */ + | 'livechat_transfer_history_fallback' + /** Sent when a livechat conversation is closed */ + | 'livechat-close' + /** Sent when a livechat conversation is started */ + | 'livechat_webrtc_video_call' + /** Sent when a livechat conversation is started */ + | 'livechat-started' + /** Sent when the priority of omnichannel is changed */ + | 'omnichannel_priority_change_history' + /** Sent when the sla of omnichannel is changed */ + | 'omnichannel_sla_change_history' + /** Sent when a chat is placed on hold */ + | 'omnichannel_placed_chat_on_hold' + /** Sent when a chat is resumed */ + | 'omnichannel_on_hold_chat_resumed' + | 'otr' + | 'otr-ack' + | 'user_joined_otr' + | 'user_requested_otr_key_refresh' + | 'user_key_refreshed_successfully' + /** Sent when the message came through e2e */ + | 'e2e' + /** Sent when a user has joined a room */ + | 'uj' + /** Sent when a user has left a room */ + | 'ul' + /** Sent when a user was removed */ + | 'ru' + /** Sent when a user was added */ + | 'au' + /** Sent when system messages were muted */ + | 'mute_unmute' + /** Sent when a room name was changed */ + | 'r' + /** Sent when a user joined a conversation */ + | 'ut' + /** Welcome message */ + | 'wm' + /** Sent when a message was removed */ + | 'rm' + /** Sent when the role of a subscription has added */ + | 'subscription-role-added' + /** Sent when the role of a subscription has removed */ + | 'subscription-role-removed' + /** Sent when the room was archived */ + | 'room-archived' + /** Sent when the room was unarchived */ + | 'room-unarchived' + /** Sent when the privacy of the room has changed */ + | 'room_changed_privacy' + /** Sent when the description of a room was changed */ + | 'room_changed_description' + /** Sent when the announcement of a room was changed */ + | 'room_changed_announcement' + /** Sent when the avatar of a room was changed */ + | 'room_changed_avatar' + /** Sent when the topic of a room was changed */ + | 'room_changed_topic' + /** Sent when e2e was enabled in a room */ + | 'room_e2e_enabled' + /** Sent when e2e was disabled in a room */ + | 'room_e2e_disabled' + /** Sent when a user was muted */ + | 'user-muted' + /** Sent when a user was unmuted */ + | 'user-unmuted' + /** Sent when a room is not readonly anymore */ + | 'room-removed-read-only' + /** Sent when a room is set to readonly */ + | 'room-set-read-only' + /** Sent when a room is set to allow reacting */ + | 'room-allowed-reacting' + /** Sent when a room is set to disallow reacting */ + | 'room-disallowed-reacting' + /** Sent on several instances when, such as when a livechat room is started */ + | 'command' + /** Sent when a message is the start of a videoconf */ + | 'videoconf' + /** Sent when a message was pinned */ + | 'message_pinned' + /** Sent when an e2e message was pinned */ + | 'message_pinned_e2e' + /** Sent when the room has a new moderator */ + | 'new-moderator' + /** Sent when a moderator was removed */ + | 'moderator-removed' + /** Sent when a room has a new owner */ + | 'new-owner' + /** Sent when an owner was removed */ + | 'owner-removed' + /** Sent when a room has a new leader */ + | 'new-leader' + /** Sent when a leader was removed */ + | 'leader-removed' + /** Sent when a user was added to a room */ + | 'discussion-created'; diff --git a/packages/apps-engine/src/definition/messages/index.ts b/packages/apps-engine/src/definition/messages/index.ts index 61bd32fb2b697..78f8a6ebe2153 100644 --- a/packages/apps-engine/src/definition/messages/index.ts +++ b/packages/apps-engine/src/definition/messages/index.ts @@ -21,6 +21,7 @@ import { IPostMessageReported } from './IPostMessageReported'; import { IPostMessageSent } from './IPostMessageSent'; import { IPostMessageStarred } from './IPostMessageStarred'; import { IPostMessageUpdated } from './IPostMessageUpdated'; +import { IPostSystemMessageSent } from './IPostSystemMessageSent'; import { IPreMessageDeletePrevent } from './IPreMessageDeletePrevent'; import { IPreMessageSentExtend } from './IPreMessageSentExtend'; import { IPreMessageSentModify } from './IPreMessageSentModify'; @@ -31,6 +32,7 @@ import { IPreMessageUpdatedPrevent } from './IPreMessageUpdatedPrevent'; import { MessageActionButtonsAlignment } from './MessageActionButtonsAlignment'; import { MessageActionType } from './MessageActionType'; import { MessageProcessingType } from './MessageProcessingType'; +import { MessageType } from './MessageType'; export { IMessage, @@ -68,4 +70,6 @@ export { MessageProcessingType, IMessageDeleteContext, Reaction, + MessageType, + IPostSystemMessageSent, }; diff --git a/packages/apps-engine/src/definition/metadata/AppInterface.ts b/packages/apps-engine/src/definition/metadata/AppInterface.ts index 9b85bdb8d275e..23afb7efb0799 100644 --- a/packages/apps-engine/src/definition/metadata/AppInterface.ts +++ b/packages/apps-engine/src/definition/metadata/AppInterface.ts @@ -7,6 +7,7 @@ export enum AppInterface { IPreMessageSentExtend = 'IPreMessageSentExtend', IPreMessageSentModify = 'IPreMessageSentModify', IPostMessageSent = 'IPostMessageSent', + IPostSystemMessageSent = 'IPostSystemMessageSent', IPreMessageDeletePrevent = 'IPreMessageDeletePrevent', IPostMessageDeleted = 'IPostMessageDeleted', IPreMessageUpdatedPrevent = 'IPreMessageUpdatedPrevent', diff --git a/packages/apps-engine/src/definition/metadata/AppMethod.ts b/packages/apps-engine/src/definition/metadata/AppMethod.ts index b7fcf306f6aa4..9cf7fb22aa4fa 100644 --- a/packages/apps-engine/src/definition/metadata/AppMethod.ts +++ b/packages/apps-engine/src/definition/metadata/AppMethod.ts @@ -30,6 +30,7 @@ export enum AppMethod { EXECUTEPREMESSAGESENTMODIFY = 'executePreMessageSentModify', CHECKPOSTMESSAGESENT = 'checkPostMessageSent', EXECUTEPOSTMESSAGESENT = 'executePostMessageSent', + EXECUTEPOSTSYSTEMMESSAGESENT = 'executePostSystemMessageSent', EXECUTEPOSTMESSAGESENTTOBOT = 'executePostMessageSentToBot', diff --git a/packages/apps-engine/src/server/managers/AppListenerManager.ts b/packages/apps-engine/src/server/managers/AppListenerManager.ts index f51adca31a921..2d6cd25e62ad6 100644 --- a/packages/apps-engine/src/server/managers/AppListenerManager.ts +++ b/packages/apps-engine/src/server/managers/AppListenerManager.ts @@ -44,6 +44,10 @@ interface IListenerExecutor { args: [IMessage]; result: IMessage; }; + [AppInterface.IPostSystemMessageSent]: { + args: [IMessage]; + result: void; + }; [AppInterface.IPostMessageSent]: { args: [IMessage]; result: void; @@ -338,6 +342,9 @@ export class AppListenerManager { case AppInterface.IPostMessageSent: this.executePostMessageSent(data as IMessage); return; + case AppInterface.IPostSystemMessageSent: + this.executePostSystemMessageSent(data as IMessage); + return; case AppInterface.IPreMessageDeletePrevent: return this.executePreMessageDeletePrevent(data as IMessage); case AppInterface.IPostMessageDeleted: @@ -560,6 +567,13 @@ export class AppListenerManager { } } + private async executePostSystemMessageSent(data: IMessage): Promise { + for (const appId of this.listeners.get(AppInterface.IPostSystemMessageSent)) { + const app = this.manager.getOneById(appId); + await app.call(AppMethod.EXECUTEPOSTSYSTEMMESSAGESENT, data); + } + } + private async executePreMessageDeletePrevent(data: IMessage): Promise { let prevented = false; diff --git a/packages/apps/src/bridges/IListenerBridge.ts b/packages/apps/src/bridges/IListenerBridge.ts index faf34118cd30b..313ecf90f5b09 100644 --- a/packages/apps/src/bridges/IListenerBridge.ts +++ b/packages/apps/src/bridges/IListenerBridge.ts @@ -19,7 +19,7 @@ declare module '@rocket.chat/apps-engine/server/bridges' { int: 'IPreMessageSentExtend' | 'IPreMessageSentModify' | 'IPreMessageUpdatedExtend' | 'IPreMessageUpdatedModify', message: IMessage, ): Promise; - messageEvent(int: 'IPostMessageSent' | 'IPostMessageUpdated', message: IMessage): Promise; + messageEvent(int: 'IPostMessageSent' | 'IPostMessageUpdated' | 'IPostSystemMessageSent', message: IMessage): Promise; roomEvent(int: 'IPreRoomUserJoined' | 'IPostRoomUserJoined', room: IRoom, joiningUser: IUser, invitingUser?: IUser): Promise; roomEvent(int: 'IPreRoomUserLeave' | 'IPostRoomUserLeave', room: IRoom, leavingUser: IUser): Promise;