From 14538f37657df2429ecb3a03d9017a987ba4b4b6 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 21 Sep 2023 17:37:04 -0300 Subject: [PATCH 01/20] send ephemeral message --- apps/meteor/app/lib/server/index.ts | 1 + .../server/startup/mentionUserNotInChannel.ts | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts diff --git a/apps/meteor/app/lib/server/index.ts b/apps/meteor/app/lib/server/index.ts index 597d03752dcc..bdd1a0bf3ce6 100644 --- a/apps/meteor/app/lib/server/index.ts +++ b/apps/meteor/app/lib/server/index.ts @@ -23,6 +23,7 @@ import './methods/deleteUserOwnAccount'; import './methods/executeSlashCommandPreview'; import './startup/filterATAllTag'; import './startup/filterATHereTag'; +import './startup/mentionUserNotInChannel'; import './methods/filterBadWords'; import './methods/getChannelHistory'; import './methods/getRoomJoinCode'; diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts new file mode 100644 index 000000000000..9ea5ebf1f333 --- /dev/null +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -0,0 +1,128 @@ +import { api } from '@rocket.chat/core-services'; +import { isDirectMessageRoom, isEditedMessage, isRoomFederated } from '@rocket.chat/core-typings'; +import { Subscriptions, Rooms } from '@rocket.chat/models'; +import type { ActionsBlock } from '@rocket.chat/ui-kit'; +// import { Meteor } from 'meteor/meteor'; +import moment from 'moment'; +// import _ from 'underscore'; + +import { callbacks } from '../../../../lib/callbacks'; +import { isTruthy } from '../../../../lib/isTruthy'; +import { i18n } from '../../../../server/lib/i18n'; +import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; + +const permissionsToAddUserToRoom = ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room']; + +const APP_ID = 'mention-core'; +const getBlocks = (commaSeparatedMentions: string) => { + return { + addUsersBlock: { + type: 'button', + appId: APP_ID, + blockId: 'actionBlock', + value: commaSeparatedMentions, + actionId: 'add-users', + text: { + type: 'plain_text', + text: i18n.t('Add_them'), + }, + }, + dismissBlock: { + type: 'button', + appId: APP_ID, + blockId: 'actionBlock', + actionId: 'dismiss', + text: { + type: 'plain_text', + text: i18n.t('Do_nothing'), + }, + }, + dmBlock: { + type: 'button', + appId: APP_ID, + value: commaSeparatedMentions, + blockId: 'actionBlock', + actionId: 'share-message', + text: { + type: 'plain_text', + text: i18n.t('Let_them_know'), + }, + }, + } as const; +}; + +callbacks.add( + 'beforeSaveMessage', + async (message) => { + // TODO: check if I need to test this 60 second rule. + // If the message was edited, or is older than 60 seconds (imported) + // the notifications will be skipped, so we can also skip this validation + if (isEditedMessage(message) || (message.ts && Math.abs(moment(message.ts).diff(moment())) > 60000) || !message.mentions) { + return message; + } + + const room = await Rooms.findOneById(message.rid); + if (!room || isDirectMessageRoom(room) || isRoomFederated(room) || room.t === 'l') { + return message; + } + + const mentions = message.mentions.filter(({ _id }) => _id !== 'all' && _id !== 'here'); + + if (!mentions.length) { + return message; + } + + const subs = await Subscriptions.findByRoomIdAndUserIds( + message.rid, + mentions.map(({ _id }) => _id), + { projection: { u: 1 } }, + ).toArray(); + + // get all users that are mentioned but not in the channel + const mentionsUsersNotInChannel = mentions.filter(({ _id }) => !subs.some((sub) => sub.u._id === _id)); + + if (!mentionsUsersNotInChannel.length) { + return message; + } + + const canAddUsers = await hasAtLeastOnePermissionAsync(message.u._id, permissionsToAddUserToRoom, message.rid); + const canDMUsers = await hasPermissionAsync(message.u._id, 'create-d'); // TODO: Perhaps check if user has DM with mentioned user (might be too expensive) + + const usernames = mentionsUsersNotInChannel.map(({ username }) => username); + const actionBlocks = getBlocks(usernames.join(',')); + + const elements: ActionsBlock['elements'] = [ + canAddUsers && actionBlocks.addUsersBlock, + (canAddUsers || canDMUsers) && actionBlocks.dismissBlock, + canDMUsers && actionBlocks.dmBlock, + ].filter(isTruthy); + + const messageLabel = canAddUsers + ? 'You_mentioned___mentions__but_theyre_not_in_this_room' + : 'You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them'; + + void api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { + msg: '', + mentions: mentionsUsersNotInChannel, + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: i18n.t(messageLabel, { mentions: `@${usernames.join(', @')}` }), // TODO: i18n + }, + } as const, + Boolean(elements.length) && + ({ + type: 'actions', + elements, + } as const), + ].filter(isTruthy), + private: true, + }); + + return message; + }, + callbacks.priority.LOW, + 'mention-user-not-in-channel', +); From 352634e1f85d9d782a3ef6c331c183358b3e0e59 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 21 Sep 2023 17:37:13 -0300 Subject: [PATCH 02/20] register app and first actions --- .../modules/core-apps/mention.module.ts | 44 +++++++++++++++++++ apps/meteor/server/startup/coreApps.ts | 2 + 2 files changed, 46 insertions(+) create mode 100644 apps/meteor/server/modules/core-apps/mention.module.ts diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts new file mode 100644 index 000000000000..7dbfb6546aa2 --- /dev/null +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -0,0 +1,44 @@ +import type { IUiKitCoreApp } from '@rocket.chat/core-services'; + +// import { VideoConf } from '@rocket.chat/core-services'; +import { addUsersToRoomMethod } from '../../../app/lib/server/methods/addUsersToRoom'; +// import { i18n } from '../../lib/i18n'; + +export class MentionModule implements IUiKitCoreApp { + appId = 'mention-core'; + + async blockAction(payload: any): Promise { + const { + // triggerId, + actionId, + payload: { value: commaSeparatedUsernames }, + // user: { _id: userId }, + } = payload; + + console.log('payload', payload); + + if (actionId === 'dismiss') { + // TODO: Remove actions from ephemeral message + // TODO: Update message after interaction. + // You mentioned Rachel Berry, but they’re not in this room. (if mentioned user is added to room remove actions) + console.log('ignore'); + // do nothing button + } + + const usernames = commaSeparatedUsernames.split(','); + if (actionId === 'add-users') { + // TODO: Remove actions from ephemeral message + console.log('add-users'); + void addUsersToRoomMethod(payload.user._id, { rid: payload.room, users: usernames }, payload.user); + // add users to channel + } + + if (actionId === 'share-message') { + // TODO: Remove actions from ephemeral message + // TODO: update ephemeral message to have the following key + // You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm + console.log('share-message'); + // let them know button + } + } +} diff --git a/apps/meteor/server/startup/coreApps.ts b/apps/meteor/server/startup/coreApps.ts index dbc4606ea38a..566e0a610124 100644 --- a/apps/meteor/server/startup/coreApps.ts +++ b/apps/meteor/server/startup/coreApps.ts @@ -1,4 +1,5 @@ import { BannerModule } from '../modules/core-apps/banner.module'; +import { MentionModule } from '../modules/core-apps/mention.module'; import { Nps } from '../modules/core-apps/nps.module'; import { VideoConfModule } from '../modules/core-apps/videoconf.module'; import { registerCoreApp } from '../services/uikit-core-app/service'; @@ -6,3 +7,4 @@ import { registerCoreApp } from '../services/uikit-core-app/service'; registerCoreApp(new Nps()); registerCoreApp(new BannerModule()); registerCoreApp(new VideoConfModule()); +registerCoreApp(new MentionModule()); From 2462d5ca493b382c1cae9e4d63a701518589f4fe Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 21 Sep 2023 17:37:18 -0300 Subject: [PATCH 03/20] translations --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index a03d4a3d6af1..0fffb468851b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -328,6 +328,7 @@ "add-team-channel_description": "Permission to add a channel to a team", "add-team-member": "Add Team Member", "add-team-member_description": "Permission to add members to a team", + "Add_them": "Add them", "add-user": "Add User", "add-user_description": "Permission to add new users to the server via users screen", "add-user-to-any-c-room": "Add User to Any Public Channel", @@ -1686,6 +1687,7 @@ "Do_not_display_unread_counter": "Do not display any counter of this channel", "Do_not_provide_this_code_to_anyone": "Do not provide this code to anyone.", "Do_Nothing": "Do Nothing", + "Do_nothing": "Do nothing", "Do_you_have_any_notes_for_this_conversation": "Do you have any notes for this conversation?", "Do_you_want_to_accept": "Do you want to accept?", "Do_you_want_to_change_to_s_question": "Do you want to change to %s?", @@ -3028,6 +3030,7 @@ "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", "Lets_get_you_new_one": "Let's get you a new one!", + "Let_them_know": "Let them know", "License": "License", "Line": "Line", "Link": "Link", @@ -5748,6 +5751,9 @@ "You_have_not_verified_your_email": "You have not verified your email.", "You_have_successfully_unsubscribed": "You have successfully unsubscribed from our Mailling List.", "You_must_join_to_view_messages_in_this_channel": "You must join to view messages in this channel", + "You_mentioned___mentions__but_theyre_not_in_this_room": "You mentioned {{mentions}}, but they're not in this room.", + "You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them": "You mentioned {{mentions}}, but they're not in this room. You can ask a room admin to add them.", + "You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm": "You mentioned {{mentions}}, but they're not in this room. You let them know via DM.", "You_need_confirm_email": "You need to confirm your email to login!", "You_need_install_an_extension_to_allow_screen_sharing": "You need install an extension to allow screen sharing", "You_need_to_change_your_password": "You need to change your password", From 06a3ebb5147d4876b27b469723c0089407492fb5 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 26 Sep 2023 15:55:33 -0300 Subject: [PATCH 04/20] wip --- .../server/startup/mentionUserNotInChannel.ts | 21 +++--- .../modules/core-apps/mention.module.ts | 70 +++++++++++++------ 2 files changed, 61 insertions(+), 30 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 9ea5ebf1f333..5eb2f94a0ad2 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -1,4 +1,5 @@ import { api } from '@rocket.chat/core-services'; +import type { IMessage } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isEditedMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import type { ActionsBlock } from '@rocket.chat/ui-kit'; @@ -14,13 +15,14 @@ import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../autho const permissionsToAddUserToRoom = ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room']; const APP_ID = 'mention-core'; -const getBlocks = (commaSeparatedMentions: string) => { +const getBlocks = (mentions: IMessage['mentions'], messageId: string) => { + const strigifiedMentions = JSON.stringify(mentions); return { addUsersBlock: { type: 'button', appId: APP_ID, - blockId: 'actionBlock', - value: commaSeparatedMentions, + blockId: messageId, + value: strigifiedMentions, actionId: 'add-users', text: { type: 'plain_text', @@ -30,7 +32,8 @@ const getBlocks = (commaSeparatedMentions: string) => { dismissBlock: { type: 'button', appId: APP_ID, - blockId: 'actionBlock', + blockId: messageId, + value: strigifiedMentions, actionId: 'dismiss', text: { type: 'plain_text', @@ -40,8 +43,8 @@ const getBlocks = (commaSeparatedMentions: string) => { dmBlock: { type: 'button', appId: APP_ID, - value: commaSeparatedMentions, - blockId: 'actionBlock', + value: strigifiedMentions, + blockId: messageId, actionId: 'share-message', text: { type: 'plain_text', @@ -89,8 +92,8 @@ callbacks.add( const canDMUsers = await hasPermissionAsync(message.u._id, 'create-d'); // TODO: Perhaps check if user has DM with mentioned user (might be too expensive) const usernames = mentionsUsersNotInChannel.map(({ username }) => username); - const actionBlocks = getBlocks(usernames.join(',')); - + const actionBlocks = getBlocks(mentionsUsersNotInChannel, message._id); + console.log(actionBlocks); const elements: ActionsBlock['elements'] = [ canAddUsers && actionBlocks.addUsersBlock, (canAddUsers || canDMUsers) && actionBlocks.dismissBlock, @@ -109,7 +112,7 @@ callbacks.add( type: 'section', text: { type: 'mrkdwn', - text: i18n.t(messageLabel, { mentions: `@${usernames.join(', @')}` }), // TODO: i18n + text: i18n.t(messageLabel, { mentions: `@${usernames.join(', @')}` }), }, } as const, Boolean(elements.length) && diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 7dbfb6546aa2..60441340b201 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -1,44 +1,72 @@ +import { api } from '@rocket.chat/core-services'; import type { IUiKitCoreApp } from '@rocket.chat/core-services'; +import type { IMessage } from '@rocket.chat/core-typings'; -// import { VideoConf } from '@rocket.chat/core-services'; import { addUsersToRoomMethod } from '../../../app/lib/server/methods/addUsersToRoom'; -// import { i18n } from '../../lib/i18n'; +import { i18n } from '../../lib/i18n'; + +const retrieveMentionsFromPayload = (stringifiedMentions: string): Exclude => { + try { + const mentions = JSON.parse(stringifiedMentions); + console.log('mentions', mentions); + if (!Array.isArray(mentions) || !mentions.length || !('username' in mentions[0])) { + throw new Error('Invalid payload'); + } + return mentions; + } catch (error) { + throw new Error('Invalid payload'); + } +}; export class MentionModule implements IUiKitCoreApp { appId = 'mention-core'; async blockAction(payload: any): Promise { const { - // triggerId, actionId, - payload: { value: commaSeparatedUsernames }, - // user: { _id: userId }, + payload: { value: stringifiedMentions }, } = payload; - console.log('payload', payload); + const mentions = retrieveMentionsFromPayload(stringifiedMentions); + + const usernames = mentions.map(({ username }) => username); + if (actionId === 'dismiss') { - // TODO: Remove actions from ephemeral message - // TODO: Update message after interaction. - // You mentioned Rachel Berry, but they’re not in this room. (if mentioned user is added to room remove actions) - console.log('ignore'); - // do nothing button + void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', { + mentions: `@${usernames.join(', @')}`, + }), + _id: payload.message, + mentions, + }); + return; } - const usernames = commaSeparatedUsernames.split(','); if (actionId === 'add-users') { - // TODO: Remove actions from ephemeral message - console.log('add-users'); - void addUsersToRoomMethod(payload.user._id, { rid: payload.room, users: usernames }, payload.user); - // add users to channel + void addUsersToRoomMethod(payload.user._id, { rid: payload.room, users: usernames as string[] }, payload.user); + void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', { + mentions: `@${usernames.join(', @')}`, + }), + _id: payload.message, + mentions, + }); + return; } if (actionId === 'share-message') { - // TODO: Remove actions from ephemeral message - // TODO: update ephemeral message to have the following key - // You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm - console.log('share-message'); - // let them know button + // const messagePayload = + // mentions.forEach( + + // ); + void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', { + mentions: `@${usernames.join(', @')}`, + }), + _id: payload.message, + mentions, + }); } } } From 859461c17b7a711a381a6f650ed16649b2c5b989 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 28 Sep 2023 13:48:19 -0300 Subject: [PATCH 05/20] Fix chat.postMessage types --- ...ookMessage.js => processWebhookMessage.ts} | 39 +++++++++++++++++-- packages/rest-typings/src/v1/chat.ts | 26 ++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) rename apps/meteor/app/lib/server/functions/{processWebhookMessage.js => processWebhookMessage.ts} (70%) diff --git a/apps/meteor/app/lib/server/functions/processWebhookMessage.js b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts similarity index 70% rename from apps/meteor/app/lib/server/functions/processWebhookMessage.js rename to apps/meteor/app/lib/server/functions/processWebhookMessage.ts index 1e5c34b287bb..5671f47adf47 100644 --- a/apps/meteor/app/lib/server/functions/processWebhookMessage.js +++ b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts @@ -1,3 +1,4 @@ +import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; @@ -7,9 +8,39 @@ import { validateRoomMessagePermissionsAsync } from '../../../authorization/serv import { getRoomByNameOrIdWithOptionToJoin } from './getRoomByNameOrIdWithOptionToJoin'; import { sendMessage } from './sendMessage'; -export const processWebhookMessage = async function (messageObj, user, defaultValues = { channel: '', alias: '', avatar: '', emoji: '' }) { +type Payload = { + channel?: string | string[]; + roomId?: string | string[]; + text?: IMessage['msg']; + msg?: IMessage['msg']; // overrided if text is present + username?: IMessage['alias']; + alias?: IMessage['alias']; // overrided if username is present + icon_emoji?: IMessage['emoji']; + emoji?: IMessage['emoji']; // overrided if icon_emoji is present + icon_url?: IMessage['avatar']; + avatar?: IMessage['avatar']; // overrided if icon_url is present + attachments?: IMessage['attachments']; + parseUrls?: boolean; + bot?: IMessage['bot']; + groupable?: IMessage['groupable']; + tmid?: IMessage['tmid']; +}; + +type DefaultValues = { + channel: string | string[]; + alias: string; + avatar: string; + emoji: string; +}; + +export const processWebhookMessage = async function ( + messageObj: Payload, + user: IUser & { username: Exclude }, + defaultValues: DefaultValues = { channel: '', alias: '', avatar: '', emoji: '' }, +) { const sentData = []; - const channels = [].concat(messageObj.channel || messageObj.roomId || defaultValues.channel); + + const channels: Array = [...new Set(messageObj.channel || messageObj.roomId || defaultValues.channel)]; for await (const channel of channels) { const channelType = channel[0]; @@ -69,7 +100,7 @@ export const processWebhookMessage = async function (messageObj, user, defaultVa messageObj.attachments = undefined; } - const message = { + const message: Partial & { parseUrls?: boolean } = { alias: messageObj.username || messageObj.alias || defaultValues.alias, msg: trim(messageObj.text || messageObj.msg || ''), attachments: messageObj.attachments || [], @@ -91,7 +122,7 @@ export const processWebhookMessage = async function (messageObj, user, defaultVa if (Array.isArray(message.attachments)) { for (let i = 0; i < message.attachments.length; i++) { - const attachment = message.attachments[i]; + const attachment = message.attachments[i] as Exclude[number] & { msg?: string }; if (attachment.msg) { attachment.text = trim(attachment.msg); delete attachment.msg; diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index b8def106c78a..ccfaa2c4a2fa 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -697,8 +697,8 @@ const ChatGetDeletedMessagesSchema = { export const isChatGetDeletedMessagesProps = ajv.compile(ChatGetDeletedMessagesSchema); type ChatPostMessage = - | { roomId: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] } - | { channel: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] }; + | { roomId: string | string[]; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] } + | { channel: string | string[]; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] }; const ChatPostMessageSchema = { oneOf: [ @@ -706,7 +706,15 @@ const ChatPostMessageSchema = { type: 'object', properties: { roomId: { - type: 'string', + oneOf: [ + { type: 'string' }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], }, text: { type: 'string', @@ -738,8 +746,16 @@ const ChatPostMessageSchema = { { type: 'object', properties: { - channel: { - type: 'string', + roomId: { + oneOf: [ + { type: 'string' }, + { + type: 'array', + items: { + type: 'string', + }, + }, + ], }, text: { type: 'string', From eabac321b1766d05a98d8e30883e5b01a4bed7cc Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 28 Sep 2023 15:50:51 -0300 Subject: [PATCH 06/20] share-message --- .../server/startup/mentionUserNotInChannel.ts | 5 ++- .../ForwardMessageModal.tsx | 1 + .../modules/core-apps/mention.module.ts | 34 +++++++++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 5eb2f94a0ad2..b22d5531e92b 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -3,9 +3,7 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isEditedMessage, isRoomFederated } from '@rocket.chat/core-typings'; import { Subscriptions, Rooms } from '@rocket.chat/models'; import type { ActionsBlock } from '@rocket.chat/ui-kit'; -// import { Meteor } from 'meteor/meteor'; import moment from 'moment'; -// import _ from 'underscore'; import { callbacks } from '../../../../lib/callbacks'; import { isTruthy } from '../../../../lib/isTruthy'; @@ -93,7 +91,6 @@ callbacks.add( const usernames = mentionsUsersNotInChannel.map(({ username }) => username); const actionBlocks = getBlocks(mentionsUsersNotInChannel, message._id); - console.log(actionBlocks); const elements: ActionsBlock['elements'] = [ canAddUsers && actionBlocks.addUsersBlock, (canAddUsers || canDMUsers) && actionBlocks.dismissBlock, @@ -104,6 +101,8 @@ callbacks.add( ? 'You_mentioned___mentions__but_theyre_not_in_this_room' : 'You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them'; + // TODO: Mentions style + // TODO: Use real name setting void api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { msg: '', mentions: mentionsUsersNotInChannel, diff --git a/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx b/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx index 27f9dd0c10ad..e8af55646149 100644 --- a/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx +++ b/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx @@ -38,6 +38,7 @@ const ForwardMessageModal = ({ onClose, permalink, message }: ForwardMessageProp const optionalMessage = ''; const curMsg = await prependReplies(optionalMessage, [message]); + // TODO: chat.postMessage accepts an array of rooms return Promise.all( rooms.map(async (roomId) => { const sendPayload = { diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 60441340b201..6a2e889dc2d6 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -1,14 +1,17 @@ import { api } from '@rocket.chat/core-services'; import type { IUiKitCoreApp } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; +import { Subscriptions } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; +import { processWebhookMessage } from '../../../app/lib/server/functions/processWebhookMessage'; import { addUsersToRoomMethod } from '../../../app/lib/server/methods/addUsersToRoom'; import { i18n } from '../../lib/i18n'; +import { roomCoordinator } from '../../lib/rooms/roomCoordinator'; const retrieveMentionsFromPayload = (stringifiedMentions: string): Exclude => { try { const mentions = JSON.parse(stringifiedMentions); - console.log('mentions', mentions); if (!Array.isArray(mentions) || !mentions.length || !('username' in mentions[0])) { throw new Error('Invalid payload'); } @@ -24,9 +27,8 @@ export class MentionModule implements IUiKitCoreApp { async blockAction(payload: any): Promise { const { actionId, - payload: { value: stringifiedMentions }, + payload: { value: stringifiedMentions, blockId: referenceMessageId }, } = payload; - console.log('payload', payload); const mentions = retrieveMentionsFromPayload(stringifiedMentions); @@ -56,10 +58,30 @@ export class MentionModule implements IUiKitCoreApp { } if (actionId === 'share-message') { - // const messagePayload = - // mentions.forEach( + const sub = await Subscriptions.findOneByRoomIdAndUserId(payload.room, payload.user._id); + // this should exist since the event is fired from withing the room (e.g the user sent a message) + if (!sub) { + throw new Error('Failed to retrieve room information'); + } + + const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid }); + if (!roomPath) { + throw new Error('Failed to retrieve path to room'); + } + + const link = new URL(Meteor.absoluteUrl(roomPath)); + link.searchParams.set('msg', referenceMessageId); + const text = `[ ](${link.toString()})`; + + // forwards message to all DMs + await processWebhookMessage( + { + roomId: mentions.map(({ _id }) => _id), + text, + }, + payload.user, + ); - // ); void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', { mentions: `@${usernames.join(', @')}`, From 2d231f659e69bfd0878e60f09839d1c3f8593820 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Wed, 11 Oct 2023 09:39:09 -0300 Subject: [PATCH 07/20] Real name and translations --- .../server/startup/mentionUserNotInChannel.ts | 29 ++++++++++++------ .../modules/core-apps/mention.module.ts | 30 +++++++++++++------ 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index b22d5531e92b..3433dabf318b 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -1,11 +1,12 @@ import { api } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; import { isDirectMessageRoom, isEditedMessage, isRoomFederated } from '@rocket.chat/core-typings'; -import { Subscriptions, Rooms } from '@rocket.chat/models'; +import { Subscriptions, Rooms, Users, Settings } from '@rocket.chat/models'; import type { ActionsBlock } from '@rocket.chat/ui-kit'; import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; +import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { isTruthy } from '../../../../lib/isTruthy'; import { i18n } from '../../../../server/lib/i18n'; import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -13,7 +14,7 @@ import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../autho const permissionsToAddUserToRoom = ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room']; const APP_ID = 'mention-core'; -const getBlocks = (mentions: IMessage['mentions'], messageId: string) => { +const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: string | undefined) => { const strigifiedMentions = JSON.stringify(mentions); return { addUsersBlock: { @@ -24,7 +25,7 @@ const getBlocks = (mentions: IMessage['mentions'], messageId: string) => { actionId: 'add-users', text: { type: 'plain_text', - text: i18n.t('Add_them'), + text: i18n.t('Add_them', undefined, lng), }, }, dismissBlock: { @@ -35,7 +36,7 @@ const getBlocks = (mentions: IMessage['mentions'], messageId: string) => { actionId: 'dismiss', text: { type: 'plain_text', - text: i18n.t('Do_nothing'), + text: i18n.t('Do_nothing', undefined, lng), }, }, dmBlock: { @@ -46,7 +47,7 @@ const getBlocks = (mentions: IMessage['mentions'], messageId: string) => { actionId: 'share-message', text: { type: 'plain_text', - text: i18n.t('Let_them_know'), + text: i18n.t('Let_them_know', undefined, lng), }, }, } as const; @@ -89,8 +90,9 @@ callbacks.add( const canAddUsers = await hasAtLeastOnePermissionAsync(message.u._id, permissionsToAddUserToRoom, message.rid); const canDMUsers = await hasPermissionAsync(message.u._id, 'create-d'); // TODO: Perhaps check if user has DM with mentioned user (might be too expensive) - const usernames = mentionsUsersNotInChannel.map(({ username }) => username); - const actionBlocks = getBlocks(mentionsUsersNotInChannel, message._id); + const { language } = (await Users.findOneById(message.u._id)) || {}; + + const actionBlocks = getBlocks(mentionsUsersNotInChannel, message._id, language); const elements: ActionsBlock['elements'] = [ canAddUsers && actionBlocks.addUsersBlock, (canAddUsers || canDMUsers) && actionBlocks.dismissBlock, @@ -101,22 +103,31 @@ callbacks.add( ? 'You_mentioned___mentions__but_theyre_not_in_this_room' : 'You_mentioned___mentions__but_theyre_not_in_this_room_You_can_ask_a_room_admin_to_add_them'; + const { value: useRealName } = (await Settings.findOneById('UI_Use_Real_Name')) || {}; + + const usernamesOrNames = mentionsUsersNotInChannel.map( + ({ username, name }) => `*@${getUserDisplayName(name, username, Boolean(useRealName))}*`, + ); + + const mentionsText = usernamesOrNames.join(', '); + // TODO: Mentions style - // TODO: Use real name setting void api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { msg: '', mentions: mentionsUsersNotInChannel, blocks: [ { + appId: APP_ID, type: 'section', text: { type: 'mrkdwn', - text: i18n.t(messageLabel, { mentions: `@${usernames.join(', @')}` }), + text: i18n.t(messageLabel, { mentions: mentionsText }, language), }, } as const, Boolean(elements.length) && ({ type: 'actions', + appId: APP_ID, elements, } as const), ].filter(isTruthy), diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 6a2e889dc2d6..4120c124b4ed 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -36,9 +36,13 @@ export class MentionModule implements IUiKitCoreApp { if (actionId === 'dismiss') { void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { - msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', { - mentions: `@${usernames.join(', @')}`, - }), + msg: i18n.t( + 'You_mentioned___mentions__but_theyre_not_in_this_room', + { + mentions: `@${usernames.join(', @')}`, + }, + payload.user.language, + ), _id: payload.message, mentions, }); @@ -48,9 +52,13 @@ export class MentionModule implements IUiKitCoreApp { if (actionId === 'add-users') { void addUsersToRoomMethod(payload.user._id, { rid: payload.room, users: usernames as string[] }, payload.user); void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { - msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', { - mentions: `@${usernames.join(', @')}`, - }), + msg: i18n.t( + 'You_mentioned___mentions__but_theyre_not_in_this_room', + { + mentions: `@${usernames.join(', @')}`, + }, + payload.user.language, + ), _id: payload.message, mentions, }); @@ -83,9 +91,13 @@ export class MentionModule implements IUiKitCoreApp { ); void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { - msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', { - mentions: `@${usernames.join(', @')}`, - }), + msg: i18n.t( + 'You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', + { + mentions: `@${usernames.join(', @')}`, + }, + payload.user.language, + ), _id: payload.message, mentions, }); From 6a7abd6e1748b0d3382463240a035f96b681915d Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Mon, 16 Oct 2023 17:43:22 -0300 Subject: [PATCH 08/20] finish tests --- .../meteor/tests/e2e/message-mentions.spec.ts | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index aa3e5b73036e..37f92b6fc367 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -1,9 +1,26 @@ +import { faker } from '@faker-js/faker'; + import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; +import { createTargetPrivateChannel } from './utils'; import { test, expect } from './utils/test'; + test.use({ storageState: Users.admin.state }); +const getMentionText = (username: string, kind?: number): string => { + if (kind === 1) { + return `You mentioned @${username}, but they're not in this room.`; + } + if (kind === 2) { + return `You mentioned @${username}, but they're not in this room. You can ask a room admin to add them.`; + } + if (kind === 3) { + return `You mentioned ${username}, but they're not in this room. You let them know via DM.`; + } + return `Hello @${username}, how are you`; +}; + test.describe.serial('message-mentions', () => { let poHomeChannel: HomeChannel; @@ -20,4 +37,180 @@ test.describe.serial('message-mentions', () => { await expect(poHomeChannel.content.messagePopupUsers.locator('role=listitem >> text="all"')).toBeVisible(); await expect(poHomeChannel.content.messagePopupUsers.locator('role=listitem >> text="here"')).toBeVisible(); }); + + test.describe('users not in channel', () => { + let targetChannel: string; + let targetChannel2: string; + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetPrivateChannel(api); + }); + + test('all actions', async ({ page }) => { + const adminPage = new HomeChannel(page); + const mentionText = getMentionText(Users.user1.data.username, 1); + + await test.step('receive bot message', async () => { + await adminPage.sidenav.openChat(targetChannel); + await adminPage.content.sendMessage(getMentionText(Users.user1.data.username)); + await expect(adminPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + + await test.step('show "Do nothing" action', async () => { + await expect(adminPage.content.lastUserMessage.locator('button >> text="Do nothing"')).toBeVisible(); + }); + await test.step('show "Add them" action', async () => { + await expect(adminPage.content.lastUserMessage.locator('button >> text="Add them"')).toBeVisible(); + }); + await test.step('show "Let them know" action', async () => { + await expect(adminPage.content.lastUserMessage.locator('button >> text="Let them know"')).toBeVisible(); + }); + + await test.step('dismiss', async () => { + await adminPage.content.lastUserMessage.locator('button >> text="Do nothing"').click(); + }); + + await test.step('receive second bot message', async () => { + await adminPage.content.sendMessage(getMentionText(Users.user1.data.username)); + await expect(adminPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('send message to users', async () => { + await adminPage.content.lastUserMessage.locator('button >> text="Let them know"').click(); + await expect(adminPage.content.lastUserMessageBody).toContainText(getMentionText(Users.user1.data.username, 3)); + }); + + await test.step('receive third bot message', async () => { + await adminPage.content.sendMessage(getMentionText(Users.user1.data.username)); + await expect(adminPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('add users to room', async () => { + await adminPage.content.lastUserMessage.locator('button >> text="Add them"').click(); + await expect(adminPage.content.lastSystemMessageBody).toContainText('added'); + }); + }); + + test.describe(() => { + test.use({ storageState: Users.user1.state }); + + test('dismiss and share message actions', async ({ page }) => { + const mentionText = getMentionText(Users.user2.data.username, 1); + const userPage = new HomeChannel(page); + + await test.step('receive bot message', async () => { + await userPage.sidenav.openChat(targetChannel); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + + await test.step('show "Do nothing" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Do nothing"')).toBeVisible(); + }); + await test.step('show "Let them know" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Let them know"')).toBeVisible(); + }); + await test.step('not show "Add them action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Add them"')).not.toBeVisible(); + }); + + await test.step('dismiss', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Do nothing"').click(); + }); + + await test.step('receive second bot message', async () => { + await userPage.sidenav.openChat(targetChannel); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('send message to users', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Let them know"').click(); + await expect(userPage.content.lastUserMessageBody).toContainText(getMentionText(Users.user2.data.username, 3)); + }); + }); + }) + + test.describe(() => { + test.use({ storageState: Users.user1.state }); + test.beforeAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin'] }] })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin', 'user', 'bot', 'app'] }] })).status()).toBe(200); + }); + + test('dismiss and add users actions', async ({ page }) => { + const mentionText = getMentionText(Users.user2.data.username, 1); + const userPage = new HomeChannel(page); + + await test.step('create private room', async () => { + targetChannel2 = faker.string.uuid(); + + await poHomeChannel.sidenav.openNewByLabel('Channel'); + await poHomeChannel.sidenav.inputChannelName.type(targetChannel2); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(page).toHaveURL(`/group/${targetChannel2}`); + }) + + await test.step('receive bot message', async () => { + await userPage.sidenav.openChat(targetChannel2); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('show "Do nothing" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Do nothing"')).toBeVisible(); + }); + await test.step('show "Add them" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Add them"')).toBeVisible(); + }); + await test.step('not show "Let them know" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Let them know"')).not.toBeVisible(); + }); + + await test.step('dismiss', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Do nothing"').click(); + }); + + await test.step('receive second bot message', async () => { + await userPage.sidenav.openChat(targetChannel2); + await userPage.content.sendMessage(getMentionText(Users.user2.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(mentionText); + }); + await test.step('add users to room', async () => { + await userPage.content.lastUserMessage.locator('button >> text="Add them"').click(); + await expect(userPage.content.lastSystemMessageBody).toContainText('added'); + }); + }); + }); + + test.describe(() => { + test.use({ storageState: Users.user2.state }); + test.beforeAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin'] }] })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/permissions.update', { permissions: [{ '_id': 'create-d', 'roles': ['admin', 'user', 'bot', 'app'] }] })).status()).toBe(200); + }); + test('no actions', async ({ page }) => { + const userPage = new HomeChannel(page); + + await test.step('receive bot message', async () => { + await userPage.sidenav.openChat(targetChannel2); + await userPage.content.sendMessage(getMentionText(Users.user3.data.username)); + await expect(userPage.content.lastUserMessage.locator('.rcx-message-block')).toContainText(getMentionText(Users.user3.data.username, 2)); + }); + + await test.step('not show "Do nothing" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Do nothing"')).not.toBeVisible(); + }); + await test.step('not show "Add them" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Add them"')).not.toBeVisible(); + }); + await test.step('not show "Let them know" action', async () => { + await expect(userPage.content.lastUserMessage.locator('button >> text="Let them know"')).not.toBeVisible(); + }); + }); + }) + + }) }); From 14990dcec7efb5fa89b93e912335e6825328aafb Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Mon, 16 Oct 2023 17:48:18 -0300 Subject: [PATCH 09/20] Fix room permission --- .../app/lib/server/startup/mentionUserNotInChannel.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 3433dabf318b..337c4c513fe0 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -9,9 +9,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; import { isTruthy } from '../../../../lib/isTruthy'; import { i18n } from '../../../../server/lib/i18n'; -import { hasAtLeastOnePermissionAsync, hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; - -const permissionsToAddUserToRoom = ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room']; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; const APP_ID = 'mention-core'; const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: string | undefined) => { @@ -87,9 +85,12 @@ callbacks.add( return message; } - const canAddUsers = await hasAtLeastOnePermissionAsync(message.u._id, permissionsToAddUserToRoom, message.rid); + const canAddUsersToThisRoom = await hasPermissionAsync(message.u._id, 'add-user-to-joined-room', message.rid); + const canAddToAnyRoom = await (room.t === 'c' + ? hasPermissionAsync(message.u._id, 'add-user-to-any-c-room') + : hasPermissionAsync(message.u._id, 'add-user-to-any-p-room')); const canDMUsers = await hasPermissionAsync(message.u._id, 'create-d'); // TODO: Perhaps check if user has DM with mentioned user (might be too expensive) - + const canAddUsers = canAddUsersToThisRoom || canAddToAnyRoom; const { language } = (await Users.findOneById(message.u._id)) || {}; const actionBlocks = getBlocks(mentionsUsersNotInChannel, message._id, language); From 80242ec8b6aac15e6769264fd96d51381e33f485 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Mon, 16 Oct 2023 17:51:23 -0300 Subject: [PATCH 10/20] undo chat.postmessage --- ...ookMessage.ts => processWebhookMessage.js} | 39 ++----------------- packages/rest-typings/src/v1/chat.ts | 26 +++---------- 2 files changed, 9 insertions(+), 56 deletions(-) rename apps/meteor/app/lib/server/functions/{processWebhookMessage.ts => processWebhookMessage.js} (70%) diff --git a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts b/apps/meteor/app/lib/server/functions/processWebhookMessage.js similarity index 70% rename from apps/meteor/app/lib/server/functions/processWebhookMessage.ts rename to apps/meteor/app/lib/server/functions/processWebhookMessage.js index 5671f47adf47..1e5c34b287bb 100644 --- a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts +++ b/apps/meteor/app/lib/server/functions/processWebhookMessage.js @@ -1,4 +1,3 @@ -import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; @@ -8,39 +7,9 @@ import { validateRoomMessagePermissionsAsync } from '../../../authorization/serv import { getRoomByNameOrIdWithOptionToJoin } from './getRoomByNameOrIdWithOptionToJoin'; import { sendMessage } from './sendMessage'; -type Payload = { - channel?: string | string[]; - roomId?: string | string[]; - text?: IMessage['msg']; - msg?: IMessage['msg']; // overrided if text is present - username?: IMessage['alias']; - alias?: IMessage['alias']; // overrided if username is present - icon_emoji?: IMessage['emoji']; - emoji?: IMessage['emoji']; // overrided if icon_emoji is present - icon_url?: IMessage['avatar']; - avatar?: IMessage['avatar']; // overrided if icon_url is present - attachments?: IMessage['attachments']; - parseUrls?: boolean; - bot?: IMessage['bot']; - groupable?: IMessage['groupable']; - tmid?: IMessage['tmid']; -}; - -type DefaultValues = { - channel: string | string[]; - alias: string; - avatar: string; - emoji: string; -}; - -export const processWebhookMessage = async function ( - messageObj: Payload, - user: IUser & { username: Exclude }, - defaultValues: DefaultValues = { channel: '', alias: '', avatar: '', emoji: '' }, -) { +export const processWebhookMessage = async function (messageObj, user, defaultValues = { channel: '', alias: '', avatar: '', emoji: '' }) { const sentData = []; - - const channels: Array = [...new Set(messageObj.channel || messageObj.roomId || defaultValues.channel)]; + const channels = [].concat(messageObj.channel || messageObj.roomId || defaultValues.channel); for await (const channel of channels) { const channelType = channel[0]; @@ -100,7 +69,7 @@ export const processWebhookMessage = async function ( messageObj.attachments = undefined; } - const message: Partial & { parseUrls?: boolean } = { + const message = { alias: messageObj.username || messageObj.alias || defaultValues.alias, msg: trim(messageObj.text || messageObj.msg || ''), attachments: messageObj.attachments || [], @@ -122,7 +91,7 @@ export const processWebhookMessage = async function ( if (Array.isArray(message.attachments)) { for (let i = 0; i < message.attachments.length; i++) { - const attachment = message.attachments[i] as Exclude[number] & { msg?: string }; + const attachment = message.attachments[i]; if (attachment.msg) { attachment.text = trim(attachment.msg); delete attachment.msg; diff --git a/packages/rest-typings/src/v1/chat.ts b/packages/rest-typings/src/v1/chat.ts index ccfaa2c4a2fa..b8def106c78a 100644 --- a/packages/rest-typings/src/v1/chat.ts +++ b/packages/rest-typings/src/v1/chat.ts @@ -697,8 +697,8 @@ const ChatGetDeletedMessagesSchema = { export const isChatGetDeletedMessagesProps = ajv.compile(ChatGetDeletedMessagesSchema); type ChatPostMessage = - | { roomId: string | string[]; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] } - | { channel: string | string[]; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] }; + | { roomId: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] } + | { channel: string; text?: string; alias?: string; emoji?: string; avatar?: string; attachments?: MessageAttachment[] }; const ChatPostMessageSchema = { oneOf: [ @@ -706,15 +706,7 @@ const ChatPostMessageSchema = { type: 'object', properties: { roomId: { - oneOf: [ - { type: 'string' }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], + type: 'string', }, text: { type: 'string', @@ -746,16 +738,8 @@ const ChatPostMessageSchema = { { type: 'object', properties: { - roomId: { - oneOf: [ - { type: 'string' }, - { - type: 'array', - items: { - type: 'string', - }, - }, - ], + channel: { + type: 'string', }, text: { type: 'string', From 8a9cc0ee9ae203b0b25e48a8e91dc975eedee8f2 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:53:17 -0300 Subject: [PATCH 11/20] Add cs --- .changeset/fresh-socks-fix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-socks-fix.md diff --git a/.changeset/fresh-socks-fix.md b/.changeset/fresh-socks-fix.md new file mode 100644 index 000000000000..004677e019b8 --- /dev/null +++ b/.changeset/fresh-socks-fix.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Mentioning users that are not in the channel now dispatches a warning message with actions From d8c6711e8726c5f8abfaf8a694b0952ff8705f5c Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 17 Oct 2023 12:33:23 -0300 Subject: [PATCH 12/20] do not add user to room automatically anymore --- .../server/lib/sendNotificationsOnMessage.js | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js index ce262e4e6756..e55be31f2d35 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js @@ -341,50 +341,7 @@ export async function sendAllNotifications(message, room) { return message; } - const { sender, hasMentionToAll, hasMentionToHere, notificationMessage, mentionIds, mentionIdsWithoutGroups } = - await sendMessageNotifications(message, room); - - // on public channels, if a mentioned user is not member of the channel yet, he will first join the channel and then be notified based on his preferences. - if (room.t === 'c') { - // get subscriptions from users already in room (to not send them a notification) - const mentions = [...mentionIdsWithoutGroups]; - const cursor = Subscriptions.findByRoomIdAndUserIds(room._id, mentionIdsWithoutGroups, { - projection: { 'u._id': 1 }, - }); - - for await (const subscription of cursor) { - const index = mentions.indexOf(subscription.u._id); - if (index !== -1) { - mentions.splice(index, 1); - } - } - - const users = await Promise.all( - mentions.map(async (userId) => { - await Room.join({ room, user: { _id: userId } }); - - return userId; - }), - ).catch((error) => { - throw new Meteor.Error(error); - }); - - const subscriptions = await Subscriptions.findByRoomIdAndUserIds(room._id, users).toArray(); - users.forEach((userId) => { - const subscription = subscriptions.find((subscription) => subscription.u._id === userId); - - void sendNotification({ - subscription, - sender, - hasMentionToAll, - hasMentionToHere, - message, - notificationMessage, - room, - mentionIds, - }); - }); - } + await sendMessageNotifications(message, room); return message; } From 16d9c41db381ea44fff1c253147899a9000a06fe Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 17 Oct 2023 12:33:37 -0300 Subject: [PATCH 13/20] Fix mention style --- apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts | 2 +- apps/meteor/tests/e2e/message-mentions.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 337c4c513fe0..7cd240c6f57f 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -107,7 +107,7 @@ callbacks.add( const { value: useRealName } = (await Settings.findOneById('UI_Use_Real_Name')) || {}; const usernamesOrNames = mentionsUsersNotInChannel.map( - ({ username, name }) => `*@${getUserDisplayName(name, username, Boolean(useRealName))}*`, + ({ username, name }) => `*${getUserDisplayName(name, username, Boolean(useRealName))}*`, ); const mentionsText = usernamesOrNames.join(', '); diff --git a/apps/meteor/tests/e2e/message-mentions.spec.ts b/apps/meteor/tests/e2e/message-mentions.spec.ts index 37f92b6fc367..7645d5b14470 100644 --- a/apps/meteor/tests/e2e/message-mentions.spec.ts +++ b/apps/meteor/tests/e2e/message-mentions.spec.ts @@ -10,10 +10,10 @@ test.use({ storageState: Users.admin.state }); const getMentionText = (username: string, kind?: number): string => { if (kind === 1) { - return `You mentioned @${username}, but they're not in this room.`; + return `You mentioned ${username}, but they're not in this room.`; } if (kind === 2) { - return `You mentioned @${username}, but they're not in this room. You can ask a room admin to add them.`; + return `You mentioned ${username}, but they're not in this room. You can ask a room admin to add them.`; } if (kind === 3) { return `You mentioned ${username}, but they're not in this room. You let them know via DM.`; From 8fc11132d6cd7d33123404a0b494f2607966d3fe Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 20 Oct 2023 10:44:09 -0300 Subject: [PATCH 14/20] remove comment --- .../room/modals/ForwardMessageModal/ForwardMessageModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx b/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx index 77f7a0832fab..d249d7137728 100644 --- a/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx +++ b/apps/meteor/client/views/room/modals/ForwardMessageModal/ForwardMessageModal.tsx @@ -38,7 +38,6 @@ const ForwardMessageModal = ({ onClose, permalink, message }: ForwardMessageProp const optionalMessage = ''; const curMsg = await prependReplies(optionalMessage, [message]); - // TODO: chat.postMessage accepts an array of rooms return Promise.all( rooms.map(async (roomId) => { const sendPayload = { From d9054d8aac8d263b63152eafd6e772c12a7dd4d9 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Fri, 20 Oct 2023 10:50:18 -0300 Subject: [PATCH 15/20] remove unused vars --- apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js index e55be31f2d35..4874b2b0e397 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.js @@ -1,6 +1,4 @@ -import { Room } from '@rocket.chat/core-services'; import { Subscriptions, Users } from '@rocket.chat/models'; -import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import { callbacks } from '../../../../lib/callbacks'; From 458aadafee53b8523bb652c4d1f4f8a9fd39f457 Mon Sep 17 00:00:00 2001 From: guijun13 Date: Thu, 26 Oct 2023 09:21:20 -0300 Subject: [PATCH 16/20] fix typo --- .../app/lib/server/startup/mentionUserNotInChannel.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index 7cd240c6f57f..cd9c3bfd543f 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -13,13 +13,13 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP const APP_ID = 'mention-core'; const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: string | undefined) => { - const strigifiedMentions = JSON.stringify(mentions); + const stringifiedMentions = JSON.stringify(mentions); return { addUsersBlock: { type: 'button', appId: APP_ID, blockId: messageId, - value: strigifiedMentions, + value: stringifiedMentions, actionId: 'add-users', text: { type: 'plain_text', @@ -30,7 +30,7 @@ const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: strin type: 'button', appId: APP_ID, blockId: messageId, - value: strigifiedMentions, + value: stringifiedMentions, actionId: 'dismiss', text: { type: 'plain_text', @@ -40,7 +40,7 @@ const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: strin dmBlock: { type: 'button', appId: APP_ID, - value: strigifiedMentions, + value: stringifiedMentions, blockId: messageId, actionId: 'share-message', text: { From 4ef3b7c554ca1b3f26df247d61533375ce593313 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 26 Oct 2023 11:01:52 -0300 Subject: [PATCH 17/20] Fix share message --- apps/meteor/server/modules/core-apps/mention.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 4120c124b4ed..34423211f23e 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -72,7 +72,7 @@ export class MentionModule implements IUiKitCoreApp { throw new Error('Failed to retrieve room information'); } - const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid }); + const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid, name: sub.name }); if (!roomPath) { throw new Error('Failed to retrieve path to room'); } From d6694f4dc2ba8a9cc0c04c5667c19f7552c2d012 Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 26 Oct 2023 11:15:57 -0300 Subject: [PATCH 18/20] fix thread mentions --- .../server/startup/mentionUserNotInChannel.ts | 1 + .../server/modules/core-apps/mention.module.ts | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index cd9c3bfd543f..f319f8ae6f26 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -116,6 +116,7 @@ callbacks.add( void api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { msg: '', mentions: mentionsUsersNotInChannel, + tmid: message.tmid, blocks: [ { appId: APP_ID, diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 34423211f23e..c9b16a6af8b8 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -1,7 +1,7 @@ import { api } from '@rocket.chat/core-services'; import type { IUiKitCoreApp } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; -import { Subscriptions } from '@rocket.chat/models'; +import { Subscriptions, Messages } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { processWebhookMessage } from '../../../app/lib/server/functions/processWebhookMessage'; @@ -34,6 +34,12 @@ export class MentionModule implements IUiKitCoreApp { const usernames = mentions.map(({ username }) => username); + const message = await Messages.findOneById(referenceMessageId); + + if (!message) { + throw new Error('Mention bot - Failed to retrieve message information'); + } + if (actionId === 'dismiss') { void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { msg: i18n.t( @@ -44,6 +50,7 @@ export class MentionModule implements IUiKitCoreApp { payload.user.language, ), _id: payload.message, + tmid: message.tmid, mentions, }); return; @@ -59,6 +66,7 @@ export class MentionModule implements IUiKitCoreApp { }, payload.user.language, ), + tmid: message.tmid, _id: payload.message, mentions, }); @@ -69,16 +77,16 @@ export class MentionModule implements IUiKitCoreApp { const sub = await Subscriptions.findOneByRoomIdAndUserId(payload.room, payload.user._id); // this should exist since the event is fired from withing the room (e.g the user sent a message) if (!sub) { - throw new Error('Failed to retrieve room information'); + throw new Error('Mention bot - Failed to retrieve room information'); } const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid, name: sub.name }); if (!roomPath) { - throw new Error('Failed to retrieve path to room'); + throw new Error('Mention bot - Failed to retrieve path to room'); } const link = new URL(Meteor.absoluteUrl(roomPath)); - link.searchParams.set('msg', referenceMessageId); + link.searchParams.set('msg', message._id); const text = `[ ](${link.toString()})`; // forwards message to all DMs @@ -98,6 +106,7 @@ export class MentionModule implements IUiKitCoreApp { }, payload.user.language, ), + tmid: message.tmid, _id: payload.message, mentions, }); From f59ba5a3571ec0139678303991834d501056968f Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Tue, 7 Nov 2023 11:08:14 -0300 Subject: [PATCH 19/20] Add extra message --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 1 + apps/meteor/server/modules/core-apps/mention.module.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 57d4f48e4285..965aa3c46fc5 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5837,6 +5837,7 @@ "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_web_browser_blocked_Rocket_Chat_from_opening_tab": "Your web browser blocked Rocket.Chat from opening a new tab.", "Your_workspace_is_ready": "Your workspace is ready to use 🎉", + "Youre_not_a_part_of__channel__and_I_mentioned_you_there": "You're not a part of {{channel}} and I mentioned you there", "Zapier": "Zapier", "registration.page.login.errors.wrongCredentials": "User not found or incorrect password", "registration.page.login.errors.invalidEmail": "Invalid Email", diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index c9b16a6af8b8..83d614f42a0d 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -85,9 +85,17 @@ export class MentionModule implements IUiKitCoreApp { throw new Error('Mention bot - Failed to retrieve path to room'); } + const messageText = i18n.t( + 'Youre_not_a_part_of__channel__and_I_mentioned_you_there', + { + channel: `#${sub.name}`, + }, + payload.user.language, + ); + const link = new URL(Meteor.absoluteUrl(roomPath)); link.searchParams.set('msg', message._id); - const text = `[ ](${link.toString()})`; + const text = `[ ](${link.toString()})\n${messageText}`; // forwards message to all DMs await processWebhookMessage( From 0248959cb930cc962e996f6c8b60500a5d2be36c Mon Sep 17 00:00:00 2001 From: gabriellsh Date: Thu, 9 Nov 2023 15:22:55 -0300 Subject: [PATCH 20/20] Review --- .../lib/server/startup/mentionUserNotInChannel.ts | 9 ++++----- .../server/modules/core-apps/mention.module.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts index f319f8ae6f26..8b8836451472 100644 --- a/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts +++ b/apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts @@ -61,14 +61,13 @@ callbacks.add( return message; } - const room = await Rooms.findOneById(message.rid); - if (!room || isDirectMessageRoom(room) || isRoomFederated(room) || room.t === 'l') { + const mentions = message.mentions.filter(({ _id }) => _id !== 'all' && _id !== 'here'); + if (!mentions.length) { return message; } - const mentions = message.mentions.filter(({ _id }) => _id !== 'all' && _id !== 'here'); - - if (!mentions.length) { + const room = await Rooms.findOneById(message.rid); + if (!room || isDirectMessageRoom(room) || isRoomFederated(room) || room.t === 'l') { return message; } diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 83d614f42a0d..ac615def8a5a 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -34,18 +34,20 @@ export class MentionModule implements IUiKitCoreApp { const usernames = mentions.map(({ username }) => username); - const message = await Messages.findOneById(referenceMessageId); + const message = await Messages.findOneById(referenceMessageId, { projection: { _id: 1, tmid: 1 } }); if (!message) { throw new Error('Mention bot - Failed to retrieve message information'); } + const joinedUsernames = `@${usernames.join(', @')}`; + if (actionId === 'dismiss') { void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { msg: i18n.t( 'You_mentioned___mentions__but_theyre_not_in_this_room', { - mentions: `@${usernames.join(', @')}`, + mentions: joinedUsernames, }, payload.user.language, ), @@ -62,7 +64,7 @@ export class MentionModule implements IUiKitCoreApp { msg: i18n.t( 'You_mentioned___mentions__but_theyre_not_in_this_room', { - mentions: `@${usernames.join(', @')}`, + mentions: joinedUsernames, }, payload.user.language, ), @@ -74,7 +76,7 @@ export class MentionModule implements IUiKitCoreApp { } if (actionId === 'share-message') { - const sub = await Subscriptions.findOneByRoomIdAndUserId(payload.room, payload.user._id); + const sub = await Subscriptions.findOneByRoomIdAndUserId(payload.room, payload.user._id, { projection: { t: 1, rid: 1, name: 1 } }); // this should exist since the event is fired from withing the room (e.g the user sent a message) if (!sub) { throw new Error('Mention bot - Failed to retrieve room information'); @@ -110,7 +112,7 @@ export class MentionModule implements IUiKitCoreApp { msg: i18n.t( 'You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', { - mentions: `@${usernames.join(', @')}`, + mentions: joinedUsernames, }, payload.user.language, ),