From 0e8e2e5cec1b0d43b18868a7ca75d10d97190940 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Thu, 7 Dec 2023 17:33:38 -0300 Subject: [PATCH] chore: convert jumpToMessage callback to class (#31101) Co-authored-by: Guilherme Gazzo --- .../server/functions/parseUrlsInMessage.ts | 1 + apps/meteor/app/oembed/server/index.ts | 1 - .../meteor/app/oembed/server/jumpToMessage.ts | 108 ---- .../messages/hooks/BeforeSaveJumpToMessage.ts | 162 ++++++ .../server/services/messages/service.ts | 31 +- .../hooks/BeforeSaveJumpToMessage.tests.ts | 500 ++++++++++++++++++ 6 files changed, 692 insertions(+), 111 deletions(-) delete mode 100644 apps/meteor/app/oembed/server/jumpToMessage.ts create mode 100644 apps/meteor/server/services/messages/hooks/BeforeSaveJumpToMessage.ts create mode 100644 apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index 2a63d024bdd9e..ea8bed9f77d46 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -4,6 +4,7 @@ import { getMessageUrlRegex } from '../../../../lib/getMessageUrlRegex'; import { Markdown } from '../../../markdown/server'; import { settings } from '../../../settings/server'; +// TODO move this function to message service to be used like a "beforeSaveMessage" hook export const parseUrlsInMessage = (message: AtLeast & { parseUrls?: boolean }, previewUrls?: string[]) => { if (message.parseUrls === false) { return message; diff --git a/apps/meteor/app/oembed/server/index.ts b/apps/meteor/app/oembed/server/index.ts index 1d33b9ff3842e..245f608c6b94e 100644 --- a/apps/meteor/app/oembed/server/index.ts +++ b/apps/meteor/app/oembed/server/index.ts @@ -1,3 +1,2 @@ -import './jumpToMessage'; import './providers'; import './server'; diff --git a/apps/meteor/app/oembed/server/jumpToMessage.ts b/apps/meteor/app/oembed/server/jumpToMessage.ts deleted file mode 100644 index 9c5be2639c7c6..0000000000000 --- a/apps/meteor/app/oembed/server/jumpToMessage.ts +++ /dev/null @@ -1,108 +0,0 @@ -import QueryString from 'querystring'; -import URL from 'url'; - -import type { MessageAttachment, IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { isQuoteAttachment } from '@rocket.chat/core-typings'; -import { Messages, Users, Rooms } from '@rocket.chat/models'; - -import { callbacks } from '../../../lib/callbacks'; -import { createQuoteAttachment } from '../../../lib/createQuoteAttachment'; -import { canAccessRoomAsync } from '../../authorization/server/functions/canAccessRoom'; -import { settings } from '../../settings/server'; -import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; - -const recursiveRemoveAttachments = (attachments: MessageAttachment, deep = 1, quoteChainLimit: number): MessageAttachment => { - if (attachments && isQuoteAttachment(attachments)) { - if (deep < quoteChainLimit - 1) { - attachments.attachments?.map((msg) => recursiveRemoveAttachments(msg, deep + 1, quoteChainLimit)); - } else { - delete attachments.attachments; - } - } - - return attachments; -}; - -const validateAttachmentDeepness = (message: IMessage): IMessage => { - if (!message?.attachments) { - return message; - } - - const quoteChainLimit = settings.get('Message_QuoteChainLimit'); - if ((message.attachments && quoteChainLimit < 2) || isNaN(quoteChainLimit)) { - delete message.attachments; - } - - message.attachments = message.attachments?.map((attachment) => recursiveRemoveAttachments(attachment, 1, quoteChainLimit)); - - return message; -}; - -callbacks.add( - 'beforeSaveMessage', - async (msg) => { - // if no message is present, or the message doesn't have any URL, skip - if (!msg?.urls?.length) { - return msg; - } - - const currentUser = await Users.findOneById(msg.u._id); - - for await (const item of msg.urls) { - // if the URL doesn't belong to the current server, skip - if (!item.url.includes(settings.get('Site_Url'))) { - continue; - } - - const urlObj = URL.parse(item.url); - - // if the URL doesn't have query params (doesn't reference message) skip - if (!urlObj.query) { - continue; - } - - const { msg: msgId } = QueryString.parse(urlObj.query); - - if (typeof msgId !== 'string') { - continue; - } - - const message = await Messages.findOneById(msgId); - - const jumpToMessage = message && validateAttachmentDeepness(message); - if (!jumpToMessage) { - continue; - } - - // validates if user can see the message - // user has to belong to the room the message was first wrote in - const room = await Rooms.findOneById(jumpToMessage.rid); - if (!room) { - continue; - } - const isLiveChatRoomVisitor = !!msg.token && !!room.v?.token && msg.token === room.v.token; - const canAccessRoomForUser = isLiveChatRoomVisitor || (currentUser && (await canAccessRoomAsync(room, currentUser))); - if (!canAccessRoomForUser) { - continue; - } - - msg.attachments = msg.attachments || []; - // Only QuoteAttachments have "message_link" property - const index = msg.attachments.findIndex((a) => isQuoteAttachment(a) && a.message_link === item.url); - if (index > -1) { - msg.attachments.splice(index, 1); - } - - const useRealName = Boolean(settings.get('UI_Use_Real_Name')); - - msg.attachments.push( - createQuoteAttachment(jumpToMessage, item.url, useRealName, getUserAvatarURL(jumpToMessage.u.username || '') as string), - ); - item.ignoreParse = true; - } - - return msg; - }, - callbacks.priority.LOW, - 'jumpToMessage', -); diff --git a/apps/meteor/server/services/messages/hooks/BeforeSaveJumpToMessage.ts b/apps/meteor/server/services/messages/hooks/BeforeSaveJumpToMessage.ts new file mode 100644 index 0000000000000..c88a27cb2b73f --- /dev/null +++ b/apps/meteor/server/services/messages/hooks/BeforeSaveJumpToMessage.ts @@ -0,0 +1,162 @@ +import QueryString from 'querystring'; +import URL from 'url'; + +import type { MessageAttachment, IMessage, IUser, IOmnichannelRoom, IRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom, isQuoteAttachment } from '@rocket.chat/core-typings'; + +import { createQuoteAttachment } from '../../../../lib/createQuoteAttachment'; + +const recursiveRemoveAttachments = (attachments: MessageAttachment, deep = 1, quoteChainLimit: number): MessageAttachment => { + if (attachments && isQuoteAttachment(attachments)) { + if (deep < quoteChainLimit - 1) { + attachments.attachments?.map((msg) => recursiveRemoveAttachments(msg, deep + 1, quoteChainLimit)); + } else { + delete attachments.attachments; + } + } + + return attachments; +}; + +const validateAttachmentDeepness = (message: IMessage, quoteChainLimit: number): IMessage => { + if (!message?.attachments) { + return message; + } + + if ((message.attachments && quoteChainLimit < 2) || isNaN(quoteChainLimit)) { + delete message.attachments; + } + + message.attachments = message.attachments?.map((attachment) => recursiveRemoveAttachments(attachment, 1, quoteChainLimit)); + + return message; +}; + +type JumpToMessageInit = { + getMessages(messageIds: IMessage['_id'][]): Promise; + getRooms(roomIds: IRoom['_id'][]): Promise; + canAccessRoom(room: IRoom, user: Pick): Promise; + getUserAvatarURL(user?: string): string; +}; + +/** + * Transform URLs in messages into quote attachments + */ +export class BeforeSaveJumpToMessage { + private getMessages: JumpToMessageInit['getMessages']; + + private getRooms: JumpToMessageInit['getRooms']; + + private canAccessRoom: JumpToMessageInit['canAccessRoom']; + + private getUserAvatarURL: JumpToMessageInit['getUserAvatarURL']; + + constructor(options: JumpToMessageInit) { + this.getMessages = options.getMessages; + this.getRooms = options.getRooms; + this.canAccessRoom = options.canAccessRoom; + this.getUserAvatarURL = options.getUserAvatarURL; + } + + async createAttachmentForMessageURLs({ + message, + user: currentUser, + config, + }: { + message: IMessage; + user: Pick; + config: { + chainLimit: number; + siteUrl: string; + useRealName: boolean; + }; + }): Promise { + // if no message is present, or the message doesn't have any URL, skip + if (!message?.urls?.length) { + return message; + } + + const linkedMessages = message.urls + .filter((item) => item.url.includes(config.siteUrl)) + .map((item) => { + const urlObj = URL.parse(item.url); + + // if the URL doesn't have query params (doesn't reference message) skip + if (!urlObj.query) { + return; + } + + const { msg: msgId } = QueryString.parse(urlObj.query); + + if (typeof msgId !== 'string') { + return; + } + + return { msgId, url: item.url }; + }) + .filter(Boolean); + + const msgs = await this.getMessages(linkedMessages.map((linkedMsg) => linkedMsg?.msgId) as string[]); + + const validMessages = msgs.filter((msg) => validateAttachmentDeepness(msg, config.chainLimit)); + + const rooms = await this.getRooms(validMessages.map((msg) => msg.rid)); + + const roomsWithPermission = + rooms && + (await Promise.all( + rooms.map(async (room) => { + if (!!message.token && isOmnichannelRoom(room) && !!room.v?.token && message.token === room.v.token) { + return room; + } + + if (currentUser && (await this.canAccessRoom(room, currentUser))) { + return room; + } + }), + )); + + const validRooms = roomsWithPermission?.filter((room) => !!room); + + const { useRealName } = config; + + const quotes = []; + + for (const item of message.urls) { + if (!item.url.includes(config.siteUrl)) { + continue; + } + + const linkedMessage = linkedMessages.find((msg) => msg?.url === item.url); + if (!linkedMessage) { + continue; + } + + const messageFromUrl = validMessages.find((msg) => msg._id === linkedMessage.msgId); + if (!messageFromUrl) { + continue; + } + + if (!validRooms?.find((room) => room?._id === messageFromUrl.rid)) { + continue; + } + + item.ignoreParse = true; + + // Only QuoteAttachments have "message_link" property + const index = message.attachments?.findIndex((a) => isQuoteAttachment(a) && a.message_link === item.url); + if (index !== undefined && index > -1) { + message.attachments?.splice(index, 1); + } + + quotes.push(createQuoteAttachment(messageFromUrl, item.url, useRealName, this.getUserAvatarURL(messageFromUrl.u.username))); + } + + if (quotes.length > 0) { + message.attachments = message.attachments || []; + message.attachments.push(...quotes); + } + + return message; + } +} diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index b59670df4edc8..84e222fec53f7 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -1,7 +1,7 @@ import type { IMessageService } from '@rocket.chat/core-services'; -import { ServiceClassInternal } from '@rocket.chat/core-services'; +import { Authorization, ServiceClassInternal } from '@rocket.chat/core-services'; import { type IMessage, type MessageTypesValues, type IUser, type IRoom, isEditedMessage } from '@rocket.chat/core-typings'; -import { Messages } from '@rocket.chat/models'; +import { Messages, Rooms } from '@rocket.chat/models'; import { deleteMessage } from '../../../app/lib/server/functions/deleteMessage'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; @@ -9,8 +9,10 @@ import { updateMessage } from '../../../app/lib/server/functions/updateMessage'; import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage'; import { executeSetReaction } from '../../../app/reactions/server/setReaction'; import { settings } from '../../../app/settings/server'; +import { getUserAvatarURL } from '../../../app/utils/server/getUserAvatarURL'; import { broadcastMessageSentEvent } from '../../modules/watchers/lib/messages'; import { BeforeSaveBadWords } from './hooks/BeforeSaveBadWords'; +import { BeforeSaveJumpToMessage } from './hooks/BeforeSaveJumpToMessage'; import { BeforeSavePreventMention } from './hooks/BeforeSavePreventMention'; import { BeforeSaveSpotify } from './hooks/BeforeSaveSpotify'; @@ -23,10 +25,26 @@ export class MessageService extends ServiceClassInternal implements IMessageServ private spotify: BeforeSaveSpotify; + private jumpToMessage: BeforeSaveJumpToMessage; + async created() { this.preventMention = new BeforeSavePreventMention(this.api); this.badWords = new BeforeSaveBadWords(); this.spotify = new BeforeSaveSpotify(); + this.jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages(messageIds) { + return Messages.findVisibleByIds(messageIds).toArray(); + }, + getRooms(roomIds) { + return Rooms.findByIds(roomIds).toArray(); + }, + canAccessRoom(room: IRoom, user: IUser): Promise { + return Authorization.canAccessRoom(room, user); + }, + getUserAvatarURL(user?: string): string { + return (user && getUserAvatarURL(user)) || ''; + }, + }); await this.configureBadWords(); } @@ -104,6 +122,15 @@ export class MessageService extends ServiceClassInternal implements IMessageServ message = await this.badWords.filterBadWords({ message }); message = await this.spotify.convertSpotifyLinks({ message }); + message = await this.jumpToMessage.createAttachmentForMessageURLs({ + message, + user, + config: { + chainLimit: settings.get('Message_QuoteChainLimit'), + siteUrl: settings.get('Site_Url'), + useRealName: settings.get('UI_Use_Real_Name'), + }, + }); if (!this.isEditedOrOld(message)) { await Promise.all([ diff --git a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts new file mode 100644 index 0000000000000..5e5a26b6b268c --- /dev/null +++ b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts @@ -0,0 +1,500 @@ +import { expect } from 'chai'; + +import { BeforeSaveJumpToMessage } from '../../../../../../server/services/messages/hooks/BeforeSaveJumpToMessage'; + +const createMessage = (msg?: string, extra: any = {}) => ({ + _id: 'random', + rid: 'GENERAL', + ts: new Date(), + u: { + _id: 'userId', + username: 'username', + }, + _updatedAt: new Date(), + msg: msg as string, + ...extra, +}); + +const createUser = (username?: string) => ({ + _id: 'userId', + username, + name: 'name', + language: 'en', +}); + +const createRoom = (extra: any = {}): any => ({ + _id: 'GENERAL', + t: 'c', + u: { + _id: 'userId', + username: 'username', + name: 'name', + }, + msgs: 1, + usersCount: 1, + _updatedAt: new Date(), + ...extra, +}); + +const countDeep = (msg: any, deep = 1): number => { + if (!msg) { + return deep - 1; + } + + if (Array.isArray(msg?.attachments) && msg.attachments.length > 0) { + return msg.attachments.reduce((count: number, att: any) => Math.max(countDeep(att, deep + 1), count), 0); + } + + return deep - 1; +}; + +describe('Create attachments for message URLs', () => { + it('should return message without attatchment and URLs if no URL provided', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey'), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + return expect(message).to.not.have.property('urls'); + }); + + it('should do nothing if URL is not from SiteUrl', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://google.com' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').of.length(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should do nothing if URL is from SiteUrl but not have a query string', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://open.rocket.chat' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').of.length(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should do nothing if URL is from SiteUrl but not have a msgId query string', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://open.rocket.chat/?token=value' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').of.length(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should do nothing if it do not find a msg from the URL', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://open.rocket.chat/?msg=value' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').of.length(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should do nothing if it cannot find the room of the message from the URL', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://open.rocket.chat/?msg=value' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').of.length(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should do nothing if user dont have access to the room of the message from the URL', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => false, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://open.rocket.chat/?msg=value' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').of.length(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should remove other attachments from the message if message_link is the same as the URL', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { + urls: [{ url: 'https://open.rocket.chat/linked?msg=linked' }], + attachments: [ + { + text: 'old attachment', + author_name: 'username', + author_icon: 'url', + message_link: 'https://open.rocket.chat/linked?msg=linked', + ts: new Date(), + }, + ], + }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(1); + + const [url] = message.urls ?? []; + + expect(url).to.include({ + url: 'https://open.rocket.chat/linked?msg=linked', + ignoreParse: true, + }); + + expect(message).to.have.property('attachments').and.to.have.lengthOf(1); + + const [attachment] = message.attachments ?? []; + + expect(attachment).to.have.property('text', 'linked message'); + }); + + it('should return an attachment with the message content if a message URL is provided', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { urls: [{ url: 'https://open.rocket.chat/linked?msg=linked' }] }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(1); + + const [url] = message.urls ?? []; + + expect(url).to.include({ + url: 'https://open.rocket.chat/linked?msg=linked', + ignoreParse: true, + }); + + expect(message).to.have.property('attachments').and.to.have.lengthOf(1); + + const [attachment] = message.attachments ?? []; + + expect(attachment).to.have.property('text', 'linked message'); + }); + + it('should respect chain limit config', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [ + createMessage('linked message', { + _id: 'linked', + attachments: [ + { + text: 'chained 1', + author_name: 'username', + author_icon: 'url', + ts: new Date(), + message_link: 'https://open.rocket.chat/linked?msg=linkedMsgId', + attachments: [ + { + text: 'chained 2', + author_name: 'username', + author_icon: 'url', + message_link: 'https://open.rocket.chat/linked?msg=linkedMsgId', + ts: new Date(), + attachments: [ + { + text: 'chained 3', + author_name: 'username', + author_icon: 'url', + message_link: 'https://open.rocket.chat/linked?msg=linkedMsgId', + ts: new Date(), + }, + ], + }, + ], + }, + ], + }), + ], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { + urls: [{ url: 'https://open.rocket.chat/linked?msg=linked' }], + }), + user: createUser(), + config: { + chainLimit: 3, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(1); + expect(message).to.have.property('attachments').and.to.have.lengthOf(1); + + const deep = countDeep(message); + expect(deep).to.be.eq(3); + }); + + it('should create the attachment if cannot access room but message has a livechat token', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom({ t: 'l', v: { token: 'livechatToken' } })], + canAccessRoom: async () => false, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { + urls: [{ url: 'https://open.rocket.chat/linked?msg=linked' }], + token: 'livechatToken', + }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(1); + + const [url] = message.urls ?? []; + + expect(url).to.include({ + url: 'https://open.rocket.chat/linked?msg=linked', + ignoreParse: true, + }); + + expect(message).to.have.property('attachments').and.to.have.lengthOf(1); + + const [attachment] = message.attachments ?? []; + + expect(attachment).to.have.property('text', 'linked message'); + }); + + it('should do nothing if cannot access room but message has a livechat token but is not from the room does not have a token', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [createMessage('linked message', { _id: 'linked' })], + getRooms: async () => [createRoom({ t: 'l' })], + canAccessRoom: async () => false, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { + urls: [{ url: 'https://open.rocket.chat/linked?msg=linked' }], + token: 'another-token', + }), + user: createUser(), + config: { + chainLimit: 10, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(1); + expect(message).to.not.have.property('attachments'); + }); + + it('should remove the clean up the attachments of the quoted message property if chain limit < 2', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => [ + createMessage('linked message', { + _id: 'linkedMsgId', + attachments: [ + { + text: 'chained 1', + author_name: 'username', + author_icon: 'url', + ts: new Date(), + }, + ], + }), + ], + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { + urls: [{ url: 'https://open.rocket.chat/linked?msg=linkedMsgId' }], + }), + user: createUser(), + config: { + chainLimit: 1, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(1); + expect(message).to.have.property('attachments').and.to.have.lengthOf(1); + + const deep = countDeep(message); + expect(deep).to.be.eq(1); + + const [attachment] = message.attachments ?? []; + + expect(attachment).to.have.property('attachments').and.to.have.lengthOf(0); + expect(attachment).to.include({ + text: 'linked message', + author_name: 'username', + author_icon: 'url', + message_link: 'https://open.rocket.chat/linked?msg=linkedMsgId', + }); + }); + + it('should work for multiple URLs', async () => { + const jumpToMessage = new BeforeSaveJumpToMessage({ + getMessages: async () => { + return [ + createMessage('first message', { + _id: 'msg1', + }), + createMessage('second message', { + _id: 'msg2', + }), + ]; + }, + getRooms: async () => [createRoom()], + canAccessRoom: async () => true, + getUserAvatarURL: () => 'url', + }); + + const message = await jumpToMessage.createAttachmentForMessageURLs({ + message: createMessage('hey', { + urls: [{ url: 'https://open.rocket.chat/linked?msg=msg1' }, { url: 'https://open.rocket.chat/linked?msg=msg2' }], + }), + user: createUser(), + config: { + chainLimit: 1, + siteUrl: 'https://open.rocket.chat', + useRealName: true, + }, + }); + + expect(message).to.have.property('urls').and.to.have.lengthOf(2); + expect(message).to.have.property('attachments').and.to.have.lengthOf(2); + + const deep = countDeep(message); + expect(deep).to.be.eq(1); + + const [att1, att2] = message.attachments ?? []; + + expect(att1).to.include({ + text: 'first message', + message_link: 'https://open.rocket.chat/linked?msg=msg1', + }); + + expect(att2).to.include({ + text: 'second message', + message_link: 'https://open.rocket.chat/linked?msg=msg2', + }); + }); +});