-
Notifications
You must be signed in to change notification settings - Fork 10.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Bot warning when mentioning users not in a channel (#30464)
Co-authored-by: Guilherme Jun Grillo <48109548+guijun13@users.noreply.github.com>
- Loading branch information
1 parent
c407aaf
commit 44dd24d
Showing
8 changed files
with
476 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@rocket.chat/meteor": minor | ||
--- | ||
|
||
Mentioning users that are not in the channel now dispatches a warning message with actions |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
apps/meteor/app/lib/server/startup/mentionUserNotInChannel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
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, 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 { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; | ||
|
||
const APP_ID = 'mention-core'; | ||
const getBlocks = (mentions: IMessage['mentions'], messageId: string, lng: string | undefined) => { | ||
const stringifiedMentions = JSON.stringify(mentions); | ||
return { | ||
addUsersBlock: { | ||
type: 'button', | ||
appId: APP_ID, | ||
blockId: messageId, | ||
value: stringifiedMentions, | ||
actionId: 'add-users', | ||
text: { | ||
type: 'plain_text', | ||
text: i18n.t('Add_them', undefined, lng), | ||
}, | ||
}, | ||
dismissBlock: { | ||
type: 'button', | ||
appId: APP_ID, | ||
blockId: messageId, | ||
value: stringifiedMentions, | ||
actionId: 'dismiss', | ||
text: { | ||
type: 'plain_text', | ||
text: i18n.t('Do_nothing', undefined, lng), | ||
}, | ||
}, | ||
dmBlock: { | ||
type: 'button', | ||
appId: APP_ID, | ||
value: stringifiedMentions, | ||
blockId: messageId, | ||
actionId: 'share-message', | ||
text: { | ||
type: 'plain_text', | ||
text: i18n.t('Let_them_know', undefined, lng), | ||
}, | ||
}, | ||
} 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 mentions = message.mentions.filter(({ _id }) => _id !== 'all' && _id !== 'here'); | ||
if (!mentions.length) { | ||
return message; | ||
} | ||
|
||
const room = await Rooms.findOneById(message.rid); | ||
if (!room || isDirectMessageRoom(room) || isRoomFederated(room) || room.t === 'l') { | ||
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 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); | ||
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'; | ||
|
||
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 | ||
void api.broadcast('notify.ephemeralMessage', message.u._id, message.rid, { | ||
msg: '', | ||
mentions: mentionsUsersNotInChannel, | ||
tmid: message.tmid, | ||
blocks: [ | ||
{ | ||
appId: APP_ID, | ||
type: 'section', | ||
text: { | ||
type: 'mrkdwn', | ||
text: i18n.t(messageLabel, { mentions: mentionsText }, language), | ||
}, | ||
} as const, | ||
Boolean(elements.length) && | ||
({ | ||
type: 'actions', | ||
appId: APP_ID, | ||
elements, | ||
} as const), | ||
].filter(isTruthy), | ||
private: true, | ||
}); | ||
|
||
return message; | ||
}, | ||
callbacks.priority.LOW, | ||
'mention-user-not-in-channel', | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
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, Messages } 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<IMessage['mentions'], undefined> => { | ||
try { | ||
const mentions = JSON.parse(stringifiedMentions); | ||
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<any> { | ||
const { | ||
actionId, | ||
payload: { value: stringifiedMentions, blockId: referenceMessageId }, | ||
} = payload; | ||
|
||
const mentions = retrieveMentionsFromPayload(stringifiedMentions); | ||
|
||
const usernames = mentions.map(({ username }) => username); | ||
|
||
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: joinedUsernames, | ||
}, | ||
payload.user.language, | ||
), | ||
_id: payload.message, | ||
tmid: message.tmid, | ||
mentions, | ||
}); | ||
return; | ||
} | ||
|
||
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: joinedUsernames, | ||
}, | ||
payload.user.language, | ||
), | ||
tmid: message.tmid, | ||
_id: payload.message, | ||
mentions, | ||
}); | ||
return; | ||
} | ||
|
||
if (actionId === 'share-message') { | ||
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'); | ||
} | ||
|
||
const roomPath = roomCoordinator.getRouteLink(sub.t, { rid: sub.rid, name: sub.name }); | ||
if (!roomPath) { | ||
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()})\n${messageText}`; | ||
|
||
// 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: joinedUsernames, | ||
}, | ||
payload.user.language, | ||
), | ||
tmid: message.tmid, | ||
_id: payload.message, | ||
mentions, | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.