import { Apps, AppEvents } from '@rocket.chat/apps'; import { Message } from '@rocket.chat/core-services'; import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync } from '../../authorization'; import { hasPermissionAsync } from '../../authorization/hasPermission'; import { callbacks } from '../../callbacks'; import { i18n } from '../../i18n'; import { isTheLastMessage } from '../../messages/isTheLastMessage'; import { notifyOnMessageChange } from '../../notifyListener'; import { emoji } from '../emoji'; export const removeUserReaction = (message: IMessage, reaction: string, username: string) => { if (!message.reactions) { return message; } const idx = message.reactions[reaction].usernames.indexOf(username); // user not found in reaction array if (idx === -1) { return message; } message.reactions[reaction].usernames.splice(idx, 1); if (!message.reactions[reaction].usernames.length) { delete message.reactions[reaction]; } return message; }; export async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction: string, userAlreadyReacted?: boolean) { await Message.beforeReacted(message, room); if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) { throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { rid: room._id, }); } if (room.ro === true && !room.reactWhenReadOnly && !(await hasPermissionAsync(user, 'post-readonly', room._id))) { // Unless the user was manually unmuted if (!(room.unmuted || []).includes(user.username as string)) { throw new Error("You can't send messages because the room is readonly."); } } let isReacted; if (userAlreadyReacted) { const oldMessage = JSON.parse(JSON.stringify(message)); removeUserReaction(message, reaction, user.username as string); if (Object.keys(message.reactions || {}).length === 0) { delete message.reactions; await Messages.unsetReactions(message._id); if (isTheLastMessage(room, message)) { await Rooms.unsetReactionsInLastMessage(room._id); } } else { await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); } } void callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact: false, oldMessage, room }); isReacted = false; } else { if (!message.reactions) { message.reactions = {}; } if (!message.reactions[reaction]) { message.reactions[reaction] = { usernames: [], }; } message.reactions[reaction].usernames.push(user.username as string); await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); } void callbacks.run('afterSetReaction', message, { user, reaction, shouldReact: true, room }); isReacted = true; } void Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); void notifyOnMessageChange({ id: message._id, }); } export async function executeSetReaction( userId: string, reaction: string, messageParam: IMessage['_id'] | IMessage, shouldReact?: boolean, ) { // Check if the emoji is valid before proceeding const reactionWithoutColons = reaction.replace(/:/g, ''); reaction = `:${reactionWithoutColons}:`; if (!emoji.list[reaction] && (await EmojiCustom.countByNameOrAlias(reactionWithoutColons)) === 0) { throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { method: 'setReaction', }); } const user = await Users.findOneById(userId); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' }); } const message = typeof messageParam === 'string' ? await Messages.findOneById(messageParam) : messageParam; if (!message) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } const userAlreadyReacted = Boolean(message.reactions?.[reaction]?.usernames?.includes(user.username as string)); // When shouldReact was not informed, toggle the reaction. if (shouldReact === undefined) { shouldReact = !userAlreadyReacted; } if (userAlreadyReacted === shouldReact) { return; } const room = await Rooms.findOneById(message.rid); if (!room) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } if (!(await canAccessRoomAsync(room, user))) { throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'setReaction' }); } return setReaction(room, user, message, reaction, userAlreadyReacted); } declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { setReaction(reaction: string, messageId: IMessage['_id'], shouldReact?: boolean): boolean | undefined; } }