From b722265bf9a65f892d1157190ac3de44db62c87f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:05:19 +0000 Subject: [PATCH 01/18] Initial plan From 88d4431630bd0bbe51de45fe3bb1dddd02854668 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:09:31 +0000 Subject: [PATCH 02/18] Add URL extraction from message AST and blocks for link previews Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .../server/functions/extractTextFromBlocks.ts | 94 +++++++++++++++++++ .../server/functions/parseUrlsInMessage.ts | 17 +++- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts diff --git a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts new file mode 100644 index 0000000000000..fca42fff5bc52 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts @@ -0,0 +1,94 @@ +import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; +import type { Root } from '@rocket.chat/message-parser'; + +/** + * Extracts all text content from message blocks (UI Kit) + * Traverses through all block types and extracts text from TextObjects (plain_text and mrkdwn) + */ +export const extractTextFromBlocks = (blocks?: MessageSurfaceLayout): string[] => { + if (!blocks || !Array.isArray(blocks)) { + return []; + } + + const textParts: string[] = []; + + const extractTextFromObject = (obj: any): void => { + if (!obj || typeof obj !== 'object') { + return; + } + + // Handle text objects (PlainText and Markdown) + if (obj.type === 'plain_text' || obj.type === 'mrkdwn') { + if (typeof obj.text === 'string') { + textParts.push(obj.text); + } + } + + // Handle arrays (e.g., fields, elements) + if (Array.isArray(obj)) { + obj.forEach((item) => extractTextFromObject(item)); + return; + } + + // Recursively check all properties + Object.values(obj).forEach((value) => { + if (value && typeof value === 'object') { + extractTextFromObject(value); + } + }); + }; + + blocks.forEach((block) => extractTextFromObject(block)); + + return textParts; +}; + +/** + * Extracts all URLs from parsed message AST (message-parser output) + * Looks for LINK nodes and extracts the src URL + */ +export const extractUrlsFromMessageAST = (md?: Root): string[] => { + if (!md || !Array.isArray(md)) { + return []; + } + + const urls: string[] = []; + + const traverse = (node: any): void => { + if (!node || typeof node !== 'object') { + return; + } + + // Handle LINK nodes - these contain the normalized URLs with proper schema + if (node.type === 'LINK' && node.value?.src?.value) { + let url = node.value.src.value; + // If URL starts with //, convert to https:// + if (url.startsWith('//')) { + url = 'https:' + url; + } + urls.push(url); + } + + // Handle arrays + if (Array.isArray(node)) { + node.forEach((item) => traverse(item)); + return; + } + + // Recursively traverse all properties + if (node.value !== undefined) { + traverse(node.value); + } + + // For objects with other properties, traverse them + Object.entries(node).forEach(([key, value]) => { + if (key !== 'type' && value && typeof value === 'object') { + traverse(value); + } + }); + }; + + traverse(md); + + return urls; +}; diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index ea8bed9f77d46..6fc66d42b187e 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -3,6 +3,7 @@ import type { IMessage, AtLeast } from '@rocket.chat/core-typings'; import { getMessageUrlRegex } from '../../../../lib/getMessageUrlRegex'; import { Markdown } from '../../../markdown/server'; import { settings } from '../../../settings/server'; +import { extractTextFromBlocks, extractUrlsFromMessageAST } from './extractTextFromBlocks'; // TODO move this function to message service to be used like a "beforeSaveMessage" hook export const parseUrlsInMessage = (message: AtLeast & { parseUrls?: boolean }, previewUrls?: string[]) => { @@ -14,7 +15,21 @@ export const parseUrlsInMessage = (message: AtLeast & { parseUr message = Markdown.code(message); const urls = message.html?.match(getMessageUrlRegex()) || []; - if (urls) { + + // Extract URLs from parsed message AST (message.md) - these have normalized schemas + if (message.md) { + const astUrls = extractUrlsFromMessageAST(message.md); + urls.push(...astUrls); + } + + // Also extract URLs from message blocks if they exist + if (message.blocks) { + const blockTexts = extractTextFromBlocks(message.blocks); + const blockUrls = blockTexts.flatMap((text) => text.match(getMessageUrlRegex()) || []); + urls.push(...blockUrls); + } + + if (urls.length > 0) { message.urls = [...new Set(urls)].map((url) => ({ url, meta: {}, From 041bb740aedd782c8f879f3f495d2103dfda530d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:11:54 +0000 Subject: [PATCH 03/18] Parse message text to extract URLs from schema-less links Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .../functions/extractTextFromBlocks.spec.ts | 181 ++++++++++++++++++ .../server/functions/parseUrlsInMessage.ts | 22 ++- 2 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts diff --git a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts new file mode 100644 index 0000000000000..09af3d53e58bf --- /dev/null +++ b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts @@ -0,0 +1,181 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; + +import { extractUrlsFromMessageAST, extractTextFromBlocks } from './extractTextFromBlocks'; + +describe('extractUrlsFromMessageAST', () => { + it('should extract URLs from LINK nodes', () => { + const md = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'LINK', + value: { + src: { + type: 'PLAIN_TEXT', + value: 'https://rocket.chat', + }, + label: [ + { + type: 'PLAIN_TEXT', + value: 'rocket.chat', + }, + ], + }, + }, + ], + }, + ]; + + const urls = extractUrlsFromMessageAST(md as any); + expect(urls).to.deep.equal(['https://rocket.chat']); + }); + + it('should convert // prefix to https://', () => { + const md = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'LINK', + value: { + src: { + type: 'PLAIN_TEXT', + value: '//github.com/RocketChat/Rocket.Chat', + }, + label: [ + { + type: 'PLAIN_TEXT', + value: 'github.com/RocketChat/Rocket.Chat', + }, + ], + }, + }, + ], + }, + ]; + + const urls = extractUrlsFromMessageAST(md as any); + expect(urls).to.deep.equal(['https://github.com/RocketChat/Rocket.Chat']); + }); + + it('should handle multiple links', () => { + const md = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'LINK', + value: { + src: { + type: 'PLAIN_TEXT', + value: 'https://rocket.chat', + }, + label: [ + { + type: 'PLAIN_TEXT', + value: 'rocket.chat', + }, + ], + }, + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'LINK', + value: { + src: { + type: 'PLAIN_TEXT', + value: '//github.com/RocketChat', + }, + label: [ + { + type: 'PLAIN_TEXT', + value: 'github.com/RocketChat', + }, + ], + }, + }, + ], + }, + ]; + + const urls = extractUrlsFromMessageAST(md as any); + expect(urls).to.deep.equal(['https://rocket.chat', 'https://github.com/RocketChat']); + }); + + it('should return empty array for undefined or non-array input', () => { + expect(extractUrlsFromMessageAST(undefined)).to.deep.equal([]); + expect(extractUrlsFromMessageAST(null as any)).to.deep.equal([]); + expect(extractUrlsFromMessageAST({} as any)).to.deep.equal([]); + }); +}); + +describe('extractTextFromBlocks', () => { + it('should extract text from plain_text blocks', () => { + const blocks = [ + { + type: 'section', + text: { + type: 'plain_text', + text: 'Hello World', + }, + }, + ]; + + const texts = extractTextFromBlocks(blocks as any); + expect(texts).to.deep.equal(['Hello World']); + }); + + it('should extract text from mrkdwn blocks', () => { + const blocks = [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'Check out https://rocket.chat', + }, + }, + ]; + + const texts = extractTextFromBlocks(blocks as any); + expect(texts).to.deep.equal(['Check out https://rocket.chat']); + }); + + it('should extract text from multiple blocks and nested elements', () => { + const blocks = [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'First block', + }, + }, + { + type: 'context', + elements: [ + { + type: 'plain_text', + text: 'Second block element 1', + }, + { + type: 'mrkdwn', + text: 'Second block element 2', + }, + ], + }, + ]; + + const texts = extractTextFromBlocks(blocks as any); + expect(texts).to.deep.equal(['First block', 'Second block element 1', 'Second block element 2']); + }); + + it('should return empty array for undefined or non-array input', () => { + expect(extractTextFromBlocks(undefined)).to.deep.equal([]); + expect(extractTextFromBlocks(null as any)).to.deep.equal([]); + expect(extractTextFromBlocks({} as any)).to.deep.equal([]); + }); +}); diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index 6fc66d42b187e..af064352793a9 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -1,4 +1,5 @@ import type { IMessage, AtLeast } from '@rocket.chat/core-typings'; +import { parse } from '@rocket.chat/message-parser'; import { getMessageUrlRegex } from '../../../../lib/getMessageUrlRegex'; import { Markdown } from '../../../markdown/server'; @@ -16,10 +17,23 @@ export const parseUrlsInMessage = (message: AtLeast & { parseUr const urls = message.html?.match(getMessageUrlRegex()) || []; - // Extract URLs from parsed message AST (message.md) - these have normalized schemas - if (message.md) { - const astUrls = extractUrlsFromMessageAST(message.md); - urls.push(...astUrls); + // Parse the message to extract URLs from links without schema + // The message parser converts links like "github.com" to proper links with "//" prefix + if (message.msg) { + try { + const customDomains = settings.get('Message_CustomDomain_AutoLink') + ? settings + .get('Message_CustomDomain_AutoLink') + .split(',') + .map((domain) => domain.trim()) + : []; + + const parsedMessage = parse(message.msg, { customDomains }); + const astUrls = extractUrlsFromMessageAST(parsedMessage); + urls.push(...astUrls); + } catch (e) { + // If parsing fails, just continue with URLs from regex + } } // Also extract URLs from message blocks if they exist From 8d8a4b6eaeee475c7828c0215b05b388dac8aee7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 05:13:08 +0000 Subject: [PATCH 04/18] Address code review feedback - improve types and error logging Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts | 4 ++-- apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts index fca42fff5bc52..d194766da6593 100644 --- a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts +++ b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts @@ -12,7 +12,7 @@ export const extractTextFromBlocks = (blocks?: MessageSurfaceLayout): string[] = const textParts: string[] = []; - const extractTextFromObject = (obj: any): void => { + const extractTextFromObject = (obj: Record | any[] | null | undefined): void => { if (!obj || typeof obj !== 'object') { return; } @@ -54,7 +54,7 @@ export const extractUrlsFromMessageAST = (md?: Root): string[] => { const urls: string[] = []; - const traverse = (node: any): void => { + const traverse = (node: Record | any[] | null | undefined): void => { if (!node || typeof node !== 'object') { return; } diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index af064352793a9..6711dfe3b12f0 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -33,6 +33,8 @@ export const parseUrlsInMessage = (message: AtLeast & { parseUr urls.push(...astUrls); } catch (e) { // If parsing fails, just continue with URLs from regex + // This can happen with malformed messages or if the parser encounters unexpected input + console.debug('Failed to parse message for URL extraction:', e); } } From fed775caaaee36e92406f87a030c68d1f09472dd Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Feb 2026 13:28:06 -0300 Subject: [PATCH 05/18] Remove deprecated extractTextFromBlocks function and add unit tests for URL extraction from message AST. Update parseUrlsInMessage to utilize new URL extraction logic. --- .../server/functions/extractTextFromBlocks.ts | 94 ------------------- ...c.ts => extractUrlsFromMessageAST.spec.ts} | 68 +------------- .../functions/extractUrlsFromMessageAST.ts | 33 +++++++ .../server/functions/parseUrlsInMessage.ts | 42 +++------ .../app/lib/server/functions/sendMessage.ts | 5 +- .../app/lib/server/functions/updateMessage.ts | 5 +- 6 files changed, 53 insertions(+), 194 deletions(-) delete mode 100644 apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts rename apps/meteor/app/lib/server/functions/{extractTextFromBlocks.spec.ts => extractUrlsFromMessageAST.spec.ts} (59%) create mode 100644 apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.ts diff --git a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts b/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts deleted file mode 100644 index d194766da6593..0000000000000 --- a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; -import type { Root } from '@rocket.chat/message-parser'; - -/** - * Extracts all text content from message blocks (UI Kit) - * Traverses through all block types and extracts text from TextObjects (plain_text and mrkdwn) - */ -export const extractTextFromBlocks = (blocks?: MessageSurfaceLayout): string[] => { - if (!blocks || !Array.isArray(blocks)) { - return []; - } - - const textParts: string[] = []; - - const extractTextFromObject = (obj: Record | any[] | null | undefined): void => { - if (!obj || typeof obj !== 'object') { - return; - } - - // Handle text objects (PlainText and Markdown) - if (obj.type === 'plain_text' || obj.type === 'mrkdwn') { - if (typeof obj.text === 'string') { - textParts.push(obj.text); - } - } - - // Handle arrays (e.g., fields, elements) - if (Array.isArray(obj)) { - obj.forEach((item) => extractTextFromObject(item)); - return; - } - - // Recursively check all properties - Object.values(obj).forEach((value) => { - if (value && typeof value === 'object') { - extractTextFromObject(value); - } - }); - }; - - blocks.forEach((block) => extractTextFromObject(block)); - - return textParts; -}; - -/** - * Extracts all URLs from parsed message AST (message-parser output) - * Looks for LINK nodes and extracts the src URL - */ -export const extractUrlsFromMessageAST = (md?: Root): string[] => { - if (!md || !Array.isArray(md)) { - return []; - } - - const urls: string[] = []; - - const traverse = (node: Record | any[] | null | undefined): void => { - if (!node || typeof node !== 'object') { - return; - } - - // Handle LINK nodes - these contain the normalized URLs with proper schema - if (node.type === 'LINK' && node.value?.src?.value) { - let url = node.value.src.value; - // If URL starts with //, convert to https:// - if (url.startsWith('//')) { - url = 'https:' + url; - } - urls.push(url); - } - - // Handle arrays - if (Array.isArray(node)) { - node.forEach((item) => traverse(item)); - return; - } - - // Recursively traverse all properties - if (node.value !== undefined) { - traverse(node.value); - } - - // For objects with other properties, traverse them - Object.entries(node).forEach(([key, value]) => { - if (key !== 'type' && value && typeof value === 'object') { - traverse(value); - } - }); - }; - - traverse(md); - - return urls; -}; diff --git a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts b/apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.spec.ts similarity index 59% rename from apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts rename to apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.spec.ts index 09af3d53e58bf..1516dbb70819c 100644 --- a/apps/meteor/app/lib/server/functions/extractTextFromBlocks.spec.ts +++ b/apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.spec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; -import { extractUrlsFromMessageAST, extractTextFromBlocks } from './extractTextFromBlocks'; +import { extractUrlsFromMessageAST } from './extractUrlsFromMessageAST'; describe('extractUrlsFromMessageAST', () => { it('should extract URLs from LINK nodes', () => { @@ -113,69 +113,3 @@ describe('extractUrlsFromMessageAST', () => { expect(extractUrlsFromMessageAST({} as any)).to.deep.equal([]); }); }); - -describe('extractTextFromBlocks', () => { - it('should extract text from plain_text blocks', () => { - const blocks = [ - { - type: 'section', - text: { - type: 'plain_text', - text: 'Hello World', - }, - }, - ]; - - const texts = extractTextFromBlocks(blocks as any); - expect(texts).to.deep.equal(['Hello World']); - }); - - it('should extract text from mrkdwn blocks', () => { - const blocks = [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: 'Check out https://rocket.chat', - }, - }, - ]; - - const texts = extractTextFromBlocks(blocks as any); - expect(texts).to.deep.equal(['Check out https://rocket.chat']); - }); - - it('should extract text from multiple blocks and nested elements', () => { - const blocks = [ - { - type: 'section', - text: { - type: 'mrkdwn', - text: 'First block', - }, - }, - { - type: 'context', - elements: [ - { - type: 'plain_text', - text: 'Second block element 1', - }, - { - type: 'mrkdwn', - text: 'Second block element 2', - }, - ], - }, - ]; - - const texts = extractTextFromBlocks(blocks as any); - expect(texts).to.deep.equal(['First block', 'Second block element 1', 'Second block element 2']); - }); - - it('should return empty array for undefined or non-array input', () => { - expect(extractTextFromBlocks(undefined)).to.deep.equal([]); - expect(extractTextFromBlocks(null as any)).to.deep.equal([]); - expect(extractTextFromBlocks({} as any)).to.deep.equal([]); - }); -}); diff --git a/apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.ts b/apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.ts new file mode 100644 index 0000000000000..222aa7c56d76d --- /dev/null +++ b/apps/meteor/app/lib/server/functions/extractUrlsFromMessageAST.ts @@ -0,0 +1,33 @@ +import type { Root } from '@rocket.chat/message-parser'; + +/** + * Extracts all URLs from parsed message AST (message-parser output) + * Looks for LINK nodes and extracts the src URL + */ +export const extractUrlsFromMessageAST = (md?: Root | Root[number] | Root[number]['value']): string[] => { + if (!md || !Array.isArray(md)) { + return []; + } + + const urls: string[] = []; + + const walk = (node: any): void => { + if (Array.isArray(node)) { + node.forEach(walk); + return; + } + if (typeof node !== 'object' || node === null) { + return; + } + if (node.type === 'LINK' && node.value?.src?.value) { + urls.push(node.value.src.value); + } + if (node.value !== undefined) { + walk(node.value); + } + }; + + walk(md); + + return urls; +}; diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index 6711dfe3b12f0..4049999625ce5 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -1,10 +1,9 @@ import type { IMessage, AtLeast } from '@rocket.chat/core-typings'; -import { parse } from '@rocket.chat/message-parser'; +import { extractUrlsFromMessageAST } from './extractUrlsFromMessageAST'; import { getMessageUrlRegex } from '../../../../lib/getMessageUrlRegex'; import { Markdown } from '../../../markdown/server'; import { settings } from '../../../settings/server'; -import { extractTextFromBlocks, extractUrlsFromMessageAST } from './extractTextFromBlocks'; // TODO move this function to message service to be used like a "beforeSaveMessage" hook export const parseUrlsInMessage = (message: AtLeast & { parseUrls?: boolean }, previewUrls?: string[]) => { @@ -15,36 +14,21 @@ export const parseUrlsInMessage = (message: AtLeast & { parseUr message.html = message.msg; message = Markdown.code(message); - const urls = message.html?.match(getMessageUrlRegex()) || []; - + const urls: string[] = []; + + // Also extract URLs from message blocks if they exist + if (message.md) { + const astUrls = extractUrlsFromMessageAST(message.md); + urls.push(...astUrls); + } + // Parse the message to extract URLs from links without schema // The message parser converts links like "github.com" to proper links with "//" prefix - if (message.msg) { - try { - const customDomains = settings.get('Message_CustomDomain_AutoLink') - ? settings - .get('Message_CustomDomain_AutoLink') - .split(',') - .map((domain) => domain.trim()) - : []; - - const parsedMessage = parse(message.msg, { customDomains }); - const astUrls = extractUrlsFromMessageAST(parsedMessage); - urls.push(...astUrls); - } catch (e) { - // If parsing fails, just continue with URLs from regex - // This can happen with malformed messages or if the parser encounters unexpected input - console.debug('Failed to parse message for URL extraction:', e); - } + if (!message.md) { + const htmlUrls = message.html?.match(getMessageUrlRegex()) || []; + urls.push(...htmlUrls); } - - // Also extract URLs from message blocks if they exist - if (message.blocks) { - const blockTexts = extractTextFromBlocks(message.blocks); - const blockUrls = blockTexts.flatMap((text) => text.match(getMessageUrlRegex()) || []); - urls.push(...blockUrls); - } - + if (urls.length > 0) { message.urls = [...new Set(urls)].map((url) => ({ url, diff --git a/apps/meteor/app/lib/server/functions/sendMessage.ts b/apps/meteor/app/lib/server/functions/sendMessage.ts index 036004aad5a7c..cda5d054cc4e5 100644 --- a/apps/meteor/app/lib/server/functions/sendMessage.ts +++ b/apps/meteor/app/lib/server/functions/sendMessage.ts @@ -257,10 +257,11 @@ export const sendMessage = async function (user: any, message: any, room: any, o } } - parseUrlsInMessage(message, previewUrls); - message = await Message.beforeSave({ message, room, user }); + // TODO: move this to the message service + parseUrlsInMessage(message, previewUrls); + if (!message) { return; } diff --git a/apps/meteor/app/lib/server/functions/updateMessage.ts b/apps/meteor/app/lib/server/functions/updateMessage.ts index baf2628e73394..1dbc898677c62 100644 --- a/apps/meteor/app/lib/server/functions/updateMessage.ts +++ b/apps/meteor/app/lib/server/functions/updateMessage.ts @@ -51,8 +51,6 @@ export const updateMessage = async function ( }, }); - parseUrlsInMessage(messageData, previewUrls); - const room = await Rooms.findOneById(messageData.rid); if (!room) { return; @@ -60,6 +58,9 @@ export const updateMessage = async function ( messageData = await Message.beforeSave({ message: messageData, room, user }); + // TODO: move this to the message service + parseUrlsInMessage(messageData, previewUrls); + if (messageData.customFields) { validateCustomMessageFields({ customFields: messageData.customFields, From cfdadc97ef242350c5d39bbb100f053aa0a1a011 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Feb 2026 16:04:45 -0300 Subject: [PATCH 06/18] Add test case for local image URL parsing in url.test.ts --- packages/message-parser/tests/url.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/message-parser/tests/url.test.ts b/packages/message-parser/tests/url.test.ts index df637b5893dc9..1595f92f8fc44 100644 --- a/packages/message-parser/tests/url.test.ts +++ b/packages/message-parser/tests/url.test.ts @@ -27,6 +27,7 @@ test.each([ ['https://rocket.chat/test?search', [paragraph([link('https://rocket.chat/test?search')])]], ['https://rocket.chat/test?search=test', [paragraph([link('https://rocket.chat/test?search=test')])]], ['https://rocket.chat', [paragraph([link('https://rocket.chat')])]], + ['http://127.0.0.1:3000/images/logo/logo.png', [paragraph([link('http://127.0.0.1:3000/images/logo/logo.png')])]], ['https://localhost', [paragraph([link('https://localhost')])]], ['https://localhost:3000', [paragraph([link('https://localhost:3000')])]], ['https://localhost:3000#fragment', [paragraph([link('https://localhost:3000#fragment')])]], From ecb75d95b7f620fb97dc5db472788c07d8804522 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Feb 2026 16:35:35 -0300 Subject: [PATCH 07/18] Refactor chat end-to-end test to use async/await and implement retry logic for oembed iframe generation --- apps/meteor/tests/end-to-end/api/chat.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 8952f9a197f54..c1a2bb74a25b4 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -5,6 +5,7 @@ import { expect } from 'chai'; import { after, before, beforeEach, describe, it } from 'mocha'; import type { Response } from 'supertest'; +import { retry } from './helpers/retry'; import { sleep } from '../../../lib/utils/sleep'; import { getCredentials, api, request, credentials, apiUrl } from '../../data/api-data'; import { followMessage, sendSimpleMessage, deleteMessage } from '../../data/chat.helper'; @@ -1257,9 +1258,9 @@ describe('[Chat]', () => { imgUrlMsgId = imgUrlResponse.body.message._id; }); - it('should have an iframe oembed with style max-width', (done) => { - setTimeout(() => { - void request + it('should have an iframe oembed with style max-width', async () => { + await retry('Oembed is generated async thats why the retry is required', async () => { + await request .get(api('chat.getMessage')) .set(credentials) .query({ @@ -1274,9 +1275,8 @@ describe('[Chat]', () => { .to.have.property('meta') .to.have.property('oembedHtml') .to.have.string('