diff --git a/app/lib/database/services/Message.ts b/app/lib/database/services/Message.ts index 1fc44d04d..172202fb7 100644 --- a/app/lib/database/services/Message.ts +++ b/app/lib/database/services/Message.ts @@ -1,3 +1,5 @@ +import { Clause } from '@nozbe/watermelondb/QueryDescription'; + import database from '..'; import { TAppDatabase } from '../interfaces'; import { MESSAGES_TABLE } from '../model/Message'; @@ -17,3 +19,14 @@ export const getMessageById = async (messageId: string | null) => { return null; } }; + +export const getMessageByQuery = async (...query: Clause[]) => { + const db = database.active; + const messageCollection = getCollection(db); + try { + const result = await messageCollection.query(...query).fetch(); + return result; + } catch (error) { + return null; + } +}; diff --git a/app/lib/encryption/helpers/createQuoteAttachment.ts b/app/lib/encryption/helpers/createQuoteAttachment.ts new file mode 100644 index 000000000..9467886d2 --- /dev/null +++ b/app/lib/encryption/helpers/createQuoteAttachment.ts @@ -0,0 +1,25 @@ +import { store } from '../../store/auxStore'; +import { IAttachment, IMessage } from '../../../definitions'; +import { getAvatarURL } from '../../methods/helpers'; + +export function createQuoteAttachment(message: IMessage, messageLink: string): IAttachment { + const { server, version: serverVersion } = store.getState().server; + const externalProviderUrl = (store.getState().settings?.Accounts_AvatarExternalProviderUrl as string) || ''; + + return { + text: message.msg, + ...('translations' in message && { translations: message?.translations }), + message_link: messageLink, + author_name: message.alias || message.u.username, + author_icon: getAvatarURL({ + avatar: message.u?.username && `/avatar/${message.u?.username}`, + type: message.t, + userId: message.u?._id, + server, + serverVersion, + externalProviderUrl + }), + attachments: message.attachments || [], + ts: message.ts + }; +} diff --git a/app/lib/encryption/helpers/getMessageUrlRegex.test.ts b/app/lib/encryption/helpers/getMessageUrlRegex.test.ts new file mode 100644 index 000000000..0a4a2359b --- /dev/null +++ b/app/lib/encryption/helpers/getMessageUrlRegex.test.ts @@ -0,0 +1,12 @@ +import { getMessageUrlRegex } from './getMessageUrlRegex'; + +describe('Should regex', () => { + test('a common quote separated by space', () => { + const quote = '[ ](https://open.rocket.chat/group/room?msg=rid) test'; + expect(quote.match(getMessageUrlRegex())).toStrictEqual(['https://open.rocket.chat/group/room?msg=rid']); + }); + test('a quote separated by break line', () => { + const quote = '[ ](https://open.rocket.chat/group/room?msg=rid)\ntest'; + expect(quote.match(getMessageUrlRegex())).toStrictEqual(['https://open.rocket.chat/group/room?msg=rid']); + }); +}); diff --git a/app/lib/encryption/helpers/getMessageUrlRegex.ts b/app/lib/encryption/helpers/getMessageUrlRegex.ts new file mode 100644 index 000000000..78e3993ef --- /dev/null +++ b/app/lib/encryption/helpers/getMessageUrlRegex.ts @@ -0,0 +1,2 @@ +export const getMessageUrlRegex = (): RegExp => + /([A-Za-z]{3,9}):\/\/([-;:&=\+\$,\w]+@{1})?([-A-Za-z0-9\.]+)+:?(\d+)?((\/[-\+=!:~%\/\.@\,\w]*)?\??([-\+=&!:;%@\/\.\,\w]+)?(?:#([^\s\)]+))?)?/g; diff --git a/app/lib/encryption/helpers/mapMessageFromApi.ts b/app/lib/encryption/helpers/mapMessageFromApi.ts new file mode 100644 index 000000000..1f662ff19 --- /dev/null +++ b/app/lib/encryption/helpers/mapMessageFromApi.ts @@ -0,0 +1,17 @@ +import { IMessage } from '../../../definitions'; + +export const mapMessageFromApi = ({ attachments, tlm, ts, _updatedAt, ...message }: IMessage) => ({ + ...message, + ts: new Date(ts), + ...(tlm && { tlm: new Date(tlm) }), + _updatedAt: new Date(_updatedAt), + // FIXME: webRtcCallEndTs doesn't exist in our interface IMessage, but exists on @rocket.chat/core-typings + // @ts-ignore + ...(message?.webRtcCallEndTs && { webRtcCallEndTs: new Date(message.webRtcCallEndTs) }), + ...(attachments && { + attachments: attachments.map(({ ts, ...attachment }) => ({ + ...(ts && { ts: new Date(ts) }), + ...(attachment as any) + })) + }) +}); diff --git a/app/lib/encryption/helpers/mapMessageFromDB.ts b/app/lib/encryption/helpers/mapMessageFromDB.ts new file mode 100644 index 000000000..cb3f24531 --- /dev/null +++ b/app/lib/encryption/helpers/mapMessageFromDB.ts @@ -0,0 +1,21 @@ +import { TMessageModel } from '../../../definitions'; +import { parseModelMessageToMessage } from './parseModelMessageToMessage'; + +export const mapMessageFromDB = (messageModel: TMessageModel) => { + const parsedMessage = parseModelMessageToMessage(messageModel); + return { + ...parsedMessage, + ts: new Date(parsedMessage.ts), + ...(parsedMessage.tlm && { tlm: new Date(parsedMessage.tlm) }), + _updatedAt: new Date(parsedMessage._updatedAt), + // FIXME: webRtcCallEndTs doesn't exist in our interface IMessage, but exists on @rocket.chat/core-typings + // @ts-ignore + ...(parsedMessage?.webRtcCallEndTs && { webRtcCallEndTs: new Date(parsedMessage.webRtcCallEndTs) }), + ...(parsedMessage.attachments && { + attachments: parsedMessage.attachments.map(({ ts, ...attachment }) => ({ + ...(ts && { ts: new Date(ts) }), + ...(attachment as any) + })) + }) + }; +}; diff --git a/app/lib/encryption/helpers/parseModelMessageToMessage.ts b/app/lib/encryption/helpers/parseModelMessageToMessage.ts new file mode 100644 index 000000000..fdb8bd521 --- /dev/null +++ b/app/lib/encryption/helpers/parseModelMessageToMessage.ts @@ -0,0 +1,42 @@ +import { IMessage, TMessageModel } from '../../../definitions'; + +export const parseModelMessageToMessage = (messageModel: TMessageModel): IMessage => + ({ + msg: messageModel.msg, + t: messageModel.t, + ts: messageModel.ts, + u: messageModel.u, + subscription: messageModel.subscription, + alias: messageModel.alias, + parseUrls: messageModel.parseUrls, + groupable: messageModel.groupable, + avatar: messageModel.avatar, + emoji: messageModel.emoji, + attachments: messageModel.attachments, + urls: messageModel.urls, + _updatedAt: messageModel._updatedAt, + status: messageModel.status, + pinned: messageModel.pinned, + starred: messageModel.starred, + editedBy: messageModel.editedBy, + reactions: messageModel.reactions, + role: messageModel.role, + drid: messageModel.drid, + dcount: messageModel.dcount, + dlm: messageModel.dlm, + tmid: messageModel.tmid, + tcount: messageModel.tcount, + tlm: messageModel.tlm, + replies: messageModel.replies, + mentions: messageModel.mentions, + channels: messageModel.channels, + unread: messageModel.unread, + autoTranslate: messageModel.autoTranslate, + translations: messageModel.translations, + tmsg: messageModel.tmsg, + blocks: messageModel.blocks, + e2e: messageModel.e2e, + tshow: messageModel.tshow, + md: messageModel.md, + comment: messageModel.comment + } as IMessage); diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index acaf71194..343e9eff6 100644 --- a/app/lib/encryption/room.ts +++ b/app/lib/encryption/room.ts @@ -2,7 +2,10 @@ import EJSON from 'ejson'; import { Base64 } from 'js-base64'; import SimpleCrypto from 'react-native-simple-crypto'; import ByteBuffer from 'bytebuffer'; +import parse from 'url-parse'; +import { Q } from '@nozbe/watermelondb'; +import getSingleMessage from '../methods/getSingleMessage'; import { IMessage, IUser } from '../../definitions'; import Deferred from './helpers/deferred'; import { debounce } from '../methods/helpers'; @@ -21,6 +24,11 @@ import { import { Encryption } from './index'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../constants'; import { Services } from '../services'; +import { getMessageUrlRegex } from './helpers/getMessageUrlRegex'; +import { mapMessageFromApi } from './helpers/mapMessageFromApi'; +import { mapMessageFromDB } from './helpers/mapMessageFromDB'; +import { createQuoteAttachment } from './helpers/createQuoteAttachment'; +import { getMessageByQuery } from '../database/services/Message'; export default class EncryptionRoom { ready: boolean; @@ -268,12 +276,15 @@ export default class EncryptionRoom { tmsg = await this.decryptText(tmsg); } - return { + const decryptedMessage: IMessage = { ...message, tmsg, msg, - e2e: E2E_STATUS.DONE + e2e: 'done' }; + + const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage); + return decryptedMessageWithQuote; } } catch { // Do nothing @@ -281,4 +292,35 @@ export default class EncryptionRoom { return message; }; + + async parseQuoteAttachment(message: IMessage) { + const urls = message.msg?.match(getMessageUrlRegex()) || []; + await Promise.all( + urls.map(async (url: string) => { + const parsedUrl = parse(url, true); + const messageId = parsedUrl.query?.msg; + if (!messageId || Array.isArray(messageId)) { + return; + } + + const messageFromDB = await getMessageByQuery(Q.and(Q.where('id', messageId), Q.where('e2e', E2E_STATUS.DONE))); + if (messageFromDB?.length) { + const decryptedQuoteMessage = mapMessageFromDB(messageFromDB[0]); + message.attachments = message.attachments || []; + const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage, url); + return message.attachments.push(quoteAttachment); + } + + const quotedMessageObject = await getSingleMessage(messageId); + if (!quotedMessageObject) { + return; + } + const decryptedQuoteMessage = await this.decrypt(mapMessageFromApi(quotedMessageObject)); + message.attachments = message.attachments || []; + const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage, url); + return message.attachments.push(quoteAttachment); + }) + ); + return message; + } }