From 1741cabe8df9e466ee4b37e470fee8f3e5c0c575 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 26 Jan 2026 17:31:03 -0300 Subject: [PATCH 01/80] test: migrate Mocha test to Jest --- .../markdown/lib/markdown.spec.ts} | 72 ++++++++++--------- apps/meteor/jest.config.ts | 5 +- apps/meteor/tests/mocks/server/meteor.ts | 52 ++++++++++++++ .../tests/unit/app/markdown/client.mocks.js | 24 ------- 4 files changed, 93 insertions(+), 60 deletions(-) rename apps/meteor/{tests/unit/app/markdown/client.tests.js => app/markdown/lib/markdown.spec.ts} (92%) create mode 100644 apps/meteor/tests/mocks/server/meteor.ts delete mode 100644 apps/meteor/tests/unit/app/markdown/client.mocks.js diff --git a/apps/meteor/tests/unit/app/markdown/client.tests.js b/apps/meteor/app/markdown/lib/markdown.spec.ts similarity index 92% rename from apps/meteor/tests/unit/app/markdown/client.tests.js rename to apps/meteor/app/markdown/lib/markdown.spec.ts index 112b83cd15112..80573e1932fa9 100644 --- a/apps/meteor/tests/unit/app/markdown/client.tests.js +++ b/apps/meteor/app/markdown/lib/markdown.spec.ts @@ -1,18 +1,39 @@ +import { jest, describe, it, expect } from '@jest/globals'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { expect } from 'chai'; -import { Markdown, original, filtered } from './client.mocks'; - -const wrapper = (text, tag) => `${tag}${text}${tag}`; -const boldWrapper = (text) => wrapper(`${text}`, '*'); -const italicWrapper = (text) => wrapper(`${text}`, '_'); -const strikeWrapper = (text) => wrapper(`${text}`, '~'); -const headerWrapper = (text, level) => `${text}`; -const quoteWrapper = (text) => +import { Markdown } from './markdown'; +import { filtered } from './parser/filtered/filtered'; +import { original } from './parser/original/original'; + +// Mock Meteor +jest.mock('meteor/meteor', () => ({ + Meteor: { + absoluteUrl() { + return 'http://localhost:3000/'; + }, + }, +})); + +// Mock Random +jest.mock('@rocket.chat/random', () => ({ + Random: { + id() { + return Math.random().toString().replace('0.', 'A'); + }, + }, +})); + +const wrapper = (text: string, tag: string) => `${tag}${text}${tag}`; +const boldWrapper = (text: string) => wrapper(`${text}`, '*'); +const italicWrapper = (text: string) => wrapper(`${text}`, '_'); +const strikeWrapper = (text: string) => wrapper(`${text}`, '~'); +const headerWrapper = (text: string, level: number) => `${text}`; +const quoteWrapper = (text: string) => `
>${text}
`; -const linkWrapped = (link, title) => `${title}`; -const inlinecodeWrapper = (text) => wrapper(`${text}`, '`'); -const codeWrapper = (text, lang) => +const linkWrapped = (link: string, title: string) => + `${title}`; +const inlinecodeWrapper = (text: string) => wrapper(`${text}`, '`'); +const codeWrapper = (text: string, lang: string) => `
\`\`\`
${text}
\`\`\`
`; const bold = { @@ -183,7 +204,7 @@ const quote = { 'He said Hello to her>': escapeHTML('He said Hello to her>'), }; -const link = { +const link: Record = { '<http://link|Text>': escapeHTML('<http://link|Text>'), '<https://open.rocket.chat/|Open Site For Rocket.Chat>': escapeHTML('<https://open.rocket.chat/|Open Site For Rocket.Chat>'), '<https://open.rocket.chat/ | Open Site For Rocket.Chat>': escapeHTML( @@ -398,9 +419,10 @@ const blockcodeFiltered = { 'Here```code```lies': 'Herecodelies', }; -const defaultObjectTest = (result, object, objectKey) => expect(result.html).to.be.equal(object[objectKey]); +const defaultObjectTest = (result: { html: string }, object: Record, objectKey: string) => + expect(result.html).toBe(object[objectKey]); -const testObject = (object, parser = original, test = defaultObjectTest) => { +const testObject = (object: Record, parser = original, test = defaultObjectTest) => { Object.keys(object).forEach((objectKey) => { describe(objectKey, () => { const message = parser === original ? { html: escapeHTML(objectKey) } : objectKey; @@ -457,23 +479,3 @@ describe('Filtered', () => { describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered)); }); - -// describe('Marked', function() { -// describe('Bold', () => testObject(bold, marked)); - -// describe('Italic', () => testObject(italic, marked)); - -// describe('Strike', () => testObject(strike, marked)); - -// describe('Headers', () => { -// describe('Level 1', () => testObject(headersLevel1, marked)); - -// describe('Level 2', () => testObject(headersLevel2, marked)); - -// describe('Level 3', () => testObject(headersLevel3, marked)); - -// describe('Level 4', () => testObject(headersLevel4, marked)); -// }); - -// describe('Quote', () => testObject(quote, marked)); -// }); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 0803852d51079..f8520778b1e49 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -39,7 +39,7 @@ export default { '/ee/app/license/server/**/*.spec.ts', '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', - '/app/utils/lib/**.spec.ts', + '/app/*/lib/**/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', '/server/services/import/**/*.spec.ts', '/server/settings/lib/**.spec.ts', @@ -48,6 +48,9 @@ export default { '/app/api/server/helpers/**.spec.ts', '/app/api/server/middlewares/**.spec.ts', ], + moduleNameMapper: { + '^meteor/(.*)': '/tests/mocks/server/meteor.ts', + }, coveragePathIgnorePatterns: ['/node_modules/'], }, ], diff --git a/apps/meteor/tests/mocks/server/meteor.ts b/apps/meteor/tests/mocks/server/meteor.ts new file mode 100644 index 0000000000000..d17b1f3691ac0 --- /dev/null +++ b/apps/meteor/tests/mocks/server/meteor.ts @@ -0,0 +1,52 @@ +import { jest } from '@jest/globals'; + +export const Meteor = { + loginWithSamlToken: jest.fn((_token, callback: () => void) => callback()), + connection: { + _stream: { on: jest.fn() }, + }, + _localStorage: { + getItem: jest.fn(), + setItem: jest.fn(), + }, + users: {}, + userId: () => 'uid', + _SynchronousQueue: class _SynchronousQueue { + drain = jest.fn(); + }, + _runFresh: jest.fn(), + _isPromise: jest.fn(() => false), + defer: jest.fn((fn: () => void) => queueMicrotask(fn)), +}; + +export const Mongo = { + Collection: class Collection { + _collection = { + _docs: {}, + }; + + find = jest.fn(() => ({ + fetch: jest.fn(() => []), + observe: jest.fn(), + })); + + findOne = jest.fn(); + + update = jest.fn(); + }, +}; + +export const Accounts = { + onLogin: jest.fn(), + onLogout: jest.fn(), +}; + +export const Tracker = { autorun: jest.fn() }; + +export const ReactiveVar = class ReactiveVar {}; + +export const EJSON = { + isBinary: jest.fn(() => false), + clone: jest.fn((obj) => obj), + equals: jest.fn((a, b) => JSON.stringify(a) === JSON.stringify(b)), +}; diff --git a/apps/meteor/tests/unit/app/markdown/client.mocks.js b/apps/meteor/tests/unit/app/markdown/client.mocks.js deleted file mode 100644 index 9ca2ec8ff8ee1..0000000000000 --- a/apps/meteor/tests/unit/app/markdown/client.mocks.js +++ /dev/null @@ -1,24 +0,0 @@ -import proxyquire from 'proxyquire'; - -const mocks = { - 'meteor/meteor': { - 'Meteor': { - absoluteUrl() { - return 'http://localhost:3000/'; - }, - }, - '@global': true, - }, - '@rocket.chat/random': { - 'Random': { - id() { - return Math.random().toString().replace('0.', 'A'); - }, - }, - '@global': true, - }, -}; - -export const { Markdown } = proxyquire.noCallThru().load('../../../../app/markdown/lib/markdown', mocks); -export const { original } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/original/original', mocks); -export const { filtered } = proxyquire.noCallThru().load('../../../../app/markdown/lib/parser/filtered/filtered', mocks); From 14617f04cedf6a1a90d4903bb956b7468120b97f Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 26 Jan 2026 21:36:00 -0300 Subject: [PATCH 02/80] refactor: convert legacy markdown lib to TypeScript --- .../app/autotranslate/server/autotranslate.ts | 2 +- .../server/functions/parseUrlsInMessage.ts | 4 +- .../app/markdown/lib/{hljs.js => hljs.ts} | 3 +- apps/meteor/app/markdown/lib/markdown.spec.ts | 58 +++++++++++-------- .../markdown/lib/{markdown.js => markdown.ts} | 47 ++++++--------- .../filtered/{filtered.js => filtered.ts} | 23 ++------ .../lib/parser/original/{code.js => code.ts} | 13 ++--- .../original/{markdown.js => markdown.ts} | 29 ++++++---- .../markdown/lib/parser/original/original.js | 25 -------- .../markdown/lib/parser/original/original.ts | 27 +++++++++ .../app/markdown/lib/parser/original/token.ts | 8 +-- 11 files changed, 116 insertions(+), 123 deletions(-) rename apps/meteor/app/markdown/lib/{hljs.js => hljs.ts} (99%) rename apps/meteor/app/markdown/lib/{markdown.js => markdown.ts} (60%) rename apps/meteor/app/markdown/lib/parser/filtered/{filtered.js => filtered.ts} (66%) rename apps/meteor/app/markdown/lib/parser/original/{code.js => code.ts} (86%) rename apps/meteor/app/markdown/lib/parser/original/{markdown.js => markdown.ts} (82%) delete mode 100644 apps/meteor/app/markdown/lib/parser/original/original.js create mode 100644 apps/meteor/app/markdown/lib/parser/original/original.ts diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 1414fd0bebf8c..2b6bbf4952043 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -228,7 +228,7 @@ export abstract class AutoTranslate { tokenizeCode(message: IMessage): IMessage { let count = message.tokens?.length || 0; message.html = message.msg; - message = Markdown.parseMessageNotEscaped(message); + message = Markdown.parseMessageNotEscaped(message as IMessage & { html: string }); // Some parsers (e. g. Marked) wrap the complete message in a

- this is unnecessary and should be ignored with respect to translations const regexWrappedParagraph = new RegExp('^\\s*

|

\\s*$', 'gm'); diff --git a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts index ea8bed9f77d46..1dd8a77c12e5c 100644 --- a/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts +++ b/apps/meteor/app/lib/server/functions/parseUrlsInMessage.ts @@ -11,7 +11,7 @@ export const parseUrlsInMessage = (message: AtLeast & { parseUr } message.html = message.msg; - message = Markdown.code(message); + message = Markdown.code(message as IMessage & { html: string }); const urls = message.html?.match(getMessageUrlRegex()) || []; if (urls) { @@ -22,7 +22,7 @@ export const parseUrlsInMessage = (message: AtLeast & { parseUr })); } - message = Markdown.mountTokensBack(message, false); + message = Markdown.mountTokensBack(message as IMessage & { html: string }, false); message.msg = message.html || message.msg; delete message.html; delete message.tokens; diff --git a/apps/meteor/app/markdown/lib/hljs.js b/apps/meteor/app/markdown/lib/hljs.ts similarity index 99% rename from apps/meteor/app/markdown/lib/hljs.js rename to apps/meteor/app/markdown/lib/hljs.ts index 1acac1409f38b..f4b15f96996e5 100644 --- a/apps/meteor/app/markdown/lib/hljs.js +++ b/apps/meteor/app/markdown/lib/hljs.ts @@ -7,7 +7,8 @@ hljs.registerLanguage('markdown', markdown); hljs.registerLanguage('clean', clean); hljs.registerLanguage('javascript', javascript); -export const register = async (lang) => { +// eslint-disable-next-line complexity +export const register = async (lang: string) => { switch (lang) { case 'onec': return hljs.registerLanguage('onec', (await import('highlight.js/lib/languages/1c')).default); diff --git a/apps/meteor/app/markdown/lib/markdown.spec.ts b/apps/meteor/app/markdown/lib/markdown.spec.ts index 80573e1932fa9..7bf0e947ba8d8 100644 --- a/apps/meteor/app/markdown/lib/markdown.spec.ts +++ b/apps/meteor/app/markdown/lib/markdown.spec.ts @@ -422,11 +422,11 @@ const blockcodeFiltered = { const defaultObjectTest = (result: { html: string }, object: Record, objectKey: string) => expect(result.html).toBe(object[objectKey]); -const testObject = (object: Record, parser = original, test = defaultObjectTest) => { +const originalTestsObject = (object: Record, test = defaultObjectTest) => { Object.keys(object).forEach((objectKey) => { describe(objectKey, () => { - const message = parser === original ? { html: escapeHTML(objectKey) } : objectKey; - const result = parser === original ? Markdown.mountTokensBack(parser(message)) : { html: parser(message) }; + const message = { html: escapeHTML(objectKey) }; + const result = Markdown.mountTokensBack(original(message)); it(`should be equal to ${object[objectKey]}`, () => { test(result, object, objectKey); }); @@ -435,47 +435,59 @@ const testObject = (object: Record, parser = original, test = de }; describe('Original', () => { - describe('Bold', () => testObject(bold)); + describe('Bold', () => originalTestsObject(bold)); - describe('Italic', () => testObject(italic)); + describe('Italic', () => originalTestsObject(italic)); - describe('Strike', () => testObject(strike)); + describe('Strike', () => originalTestsObject(strike)); describe('Headers', () => { - describe('Level 1', () => testObject(headersLevel1)); + describe('Level 1', () => originalTestsObject(headersLevel1)); - describe('Level 2', () => testObject(headersLevel2)); + describe('Level 2', () => originalTestsObject(headersLevel2)); - describe('Level 3', () => testObject(headersLevel3)); + describe('Level 3', () => originalTestsObject(headersLevel3)); - describe('Level 4', () => testObject(headersLevel4)); + describe('Level 4', () => originalTestsObject(headersLevel4)); }); - describe('Quote', () => testObject(quote)); + describe('Quote', () => originalTestsObject(quote)); - describe('Link', () => testObject(link)); + describe('Link', () => originalTestsObject(link)); - describe('Inline Code', () => testObject(inlinecode)); + describe('Inline Code', () => originalTestsObject(inlinecode)); - describe('Code', () => testObject(code)); + describe('Code', () => originalTestsObject(code)); - describe('Nested', () => testObject(nested)); + describe('Nested', () => originalTestsObject(nested)); }); +const filteredTestsObject = (object: Record, test = defaultObjectTest) => { + Object.keys(object).forEach((objectKey) => { + describe(objectKey, () => { + const message = objectKey; + const result = { html: filtered(message) }; + it(`should be equal to ${object[objectKey]}`, () => { + test(result, object, objectKey); + }); + }); + }); +}; + describe('Filtered', () => { - describe('BoldFilter', () => testObject(boldFiltered, filtered)); + describe('BoldFilter', () => filteredTestsObject(boldFiltered)); - describe('Italic', () => testObject(italicFiltered, filtered)); + describe('Italic', () => filteredTestsObject(italicFiltered)); - describe('StrikeFilter', () => testObject(strikeFiltered, filtered)); + describe('StrikeFilter', () => filteredTestsObject(strikeFiltered)); - describe('HeadingFilter', () => testObject(headingFiltered, filtered)); + describe('HeadingFilter', () => filteredTestsObject(headingFiltered)); - describe('QuoteFilter', () => testObject(quoteFiltered, filtered)); + describe('QuoteFilter', () => filteredTestsObject(quoteFiltered)); - describe('LinkFilter', () => testObject(linkFiltered, filtered)); + describe('LinkFilter', () => filteredTestsObject(linkFiltered)); - describe('inlinecodeFilter', () => testObject(inlinecodeFiltered, filtered)); + describe('inlinecodeFilter', () => filteredTestsObject(inlinecodeFiltered)); - describe('blockcodeFilter', () => testObject(blockcodeFiltered, filtered)); + describe('blockcodeFilter', () => filteredTestsObject(blockcodeFiltered)); }); diff --git a/apps/meteor/app/markdown/lib/markdown.js b/apps/meteor/app/markdown/lib/markdown.ts similarity index 60% rename from apps/meteor/app/markdown/lib/markdown.js rename to apps/meteor/app/markdown/lib/markdown.ts index c7fe452e08291..4de937ed18840 100644 --- a/apps/meteor/app/markdown/lib/markdown.js +++ b/apps/meteor/app/markdown/lib/markdown.ts @@ -1,7 +1,4 @@ -/* - * Markdown is a named function that will parse markdown syntax - * @param {Object} message - The message object - */ +import type { Token } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -9,41 +6,36 @@ import { filtered } from './parser/filtered/filtered'; import { code } from './parser/original/code'; import { original } from './parser/original/original'; -const parsers = { - original, - filtered, -}; - class MarkdownClass { - parse(text) { + parse(text: string) { const message = { html: escapeHTML(text), }; return this.mountTokensBack(this.parseMessageNotEscaped(message)).html; } - parseNotEscaped(text) { + parseNotEscaped(text: string) { const message = { html: text, }; return this.mountTokensBack(this.parseMessageNotEscaped(message)).html; } - parseMessageNotEscaped(message) { + parseMessageNotEscaped(message: TMessage) { const options = { rootUrl: Meteor.absoluteUrl(), }; - return parsers.original(message, options); + return original(message, options); } - mountTokensBackRecursively(message, tokenList, useHtml = true) { + mountTokensBackRecursively(message: { html: string; tokens?: Token[] }, tokenList: Token[], useHtml = true) { const missingTokens = []; if (tokenList.length > 0) { for (const { token, text, noHtml } of tokenList) { if (message.html.indexOf(token) >= 0) { - message.html = message.html.replace(token, () => (useHtml ? text : noHtml)); // Uses lambda so doesn't need to escape $ + message.html = message.html.replace(token, () => (useHtml ? text : (noHtml ?? ''))); // Uses lambda so doesn't need to escape $ } else { missingTokens.push({ token, text, noHtml }); } @@ -57,7 +49,7 @@ class MarkdownClass { } } - mountTokensBack(message, useHtml = true) { + mountTokensBack(message: TMessage, useHtml = true) { if (message.tokens) { this.mountTokensBackRecursively(message, message.tokens, useHtml); } @@ -65,30 +57,27 @@ class MarkdownClass { return message; } - code(...args) { - return code(...args); + code(message: TMessage) { + return code(message); } - /** @param {string} message */ - filterMarkdownFromMessage(message) { - return parsers.filtered(message); + filterMarkdownFromMessage(message: string) { + return filtered(message); } } export const Markdown = new MarkdownClass(); -/** @param {string} message */ -export const filterMarkdown = (message) => Markdown.filterMarkdownFromMessage(message); +export const filterMarkdown = (message: string) => Markdown.filterMarkdownFromMessage(message); -export const createMarkdownMessageRenderer = ({ ...options }) => { - const markedParser = parsers.marked; - return (message, useMarkedParser = false) => { +export const createMarkdownMessageRenderer = + ({ ...options }) => + (message: { html: string; tokens?: Token[] }) => { if (!message?.html?.trim()) { return message; } - return useMarkedParser ? markedParser(message, options) : parsers.original(message, options); + return original(message, options); }; -}; -export const createMarkdownNotificationRenderer = () => (message) => parsers.filtered(message); +export const createMarkdownNotificationRenderer = () => (message: string) => filtered(message); diff --git a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js b/apps/meteor/app/markdown/lib/parser/filtered/filtered.ts similarity index 66% rename from apps/meteor/app/markdown/lib/parser/filtered/filtered.js rename to apps/meteor/app/markdown/lib/parser/filtered/filtered.ts index 260fc835d8a0a..947f26c725672 100644 --- a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js +++ b/apps/meteor/app/markdown/lib/parser/filtered/filtered.ts @@ -1,30 +1,17 @@ -/** - * Filter markdown tags in message - * Use case: notifications - * @param {string} message - */ -export const filtered = ( - message, - options = { - supportSchemesForLink: 'http,https', - }, -) => { - const schemes = options.supportSchemesForLink.split(',').join('|'); +export const filtered = (message: string) => { + const schemes = 'http|https'; // Remove block code backticks message = message.replace(/```/g, ''); // Remove inline code backticks - message = message.replace(new RegExp(/`([^`\r\n]+)\`/gm), (match) => match.substr(1, match.length - 2)); + message = message.replace(new RegExp(/`([^`\r\n]+)\`/gm), (match) => match.slice(1, -1)); // Filter [text](url), ![alt_text](image_url) - message = message.replace(new RegExp(`!?\\[([^\\]]+)\\]\\((?:${schemes}):\\/\\/[^\\)]+\\)`, 'gm'), (match, title) => title); + message = message.replace(new RegExp(`!?\\[([^\\]]+)\\]\\((?:${schemes}):\\/\\/[^\\)]+\\)`, 'gm'), (_, title) => title); // Filter - message = message.replace( - new RegExp(`(?:<|<)(?:${schemes}):\\/\\/[^\\|]+\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), - (match, title) => title, - ); + message = message.replace(new RegExp(`(?:<|<)(?:${schemes}):\\/\\/[^\\|]+\\|(.+?)(?=>|>)(?:>|>)`, 'gm'), (_, title) => title); // Filter headings message = message.replace( diff --git a/apps/meteor/app/markdown/lib/parser/original/code.js b/apps/meteor/app/markdown/lib/parser/original/code.ts similarity index 86% rename from apps/meteor/app/markdown/lib/parser/original/code.js rename to apps/meteor/app/markdown/lib/parser/original/code.ts index 2bb2ef603b965..3884263f814e9 100644 --- a/apps/meteor/app/markdown/lib/parser/original/code.js +++ b/apps/meteor/app/markdown/lib/parser/original/code.ts @@ -1,13 +1,10 @@ -/* - * code() is a named function that will parse `inline code` and ```codeblock``` syntaxes - * @param {Object} message - The message object - */ +import type { Token } from '@rocket.chat/core-typings'; import { unescapeHTML } from '@rocket.chat/string-helpers'; import { addAsToken } from './token'; import hljs, { register } from '../../hljs'; -const inlinecode = (message) => { +const inlinecode = (message: { html: string; tokens?: Token[] }) => { // Support `text` message.html = message.html.replace(/\`([^`\r\n]+)\`([<_*~]|\B|\b|$)/gm, (match, p1, p2) => addAsToken( @@ -19,7 +16,7 @@ const inlinecode = (message) => { ); }; -const codeblocks = (message) => { +const codeblocks = (message: { msg?: string; html: string; tokens?: Token[] }) => { // Count occurencies of ``` const count = (message.html.match(/```/gm) || []).length; @@ -48,7 +45,7 @@ const codeblocks = (message) => { const result = (() => { if (lang) { try { - register(lang); + void register(lang); return hljs.highlight(lang, code); } catch (error) { console.error(error); @@ -77,7 +74,7 @@ const codeblocks = (message) => { } }; -export const code = (message) => { +export const code = (message: TMessage) => { if (message.html?.trim()) { codeblocks(message); inlinecode(message); diff --git a/apps/meteor/app/markdown/lib/parser/original/markdown.js b/apps/meteor/app/markdown/lib/parser/original/markdown.ts similarity index 82% rename from apps/meteor/app/markdown/lib/parser/original/markdown.js rename to apps/meteor/app/markdown/lib/parser/original/markdown.ts index 9d619f74ab010..ca2e42024ef49 100644 --- a/apps/meteor/app/markdown/lib/parser/original/markdown.js +++ b/apps/meteor/app/markdown/lib/parser/original/markdown.ts @@ -1,6 +1,8 @@ +import type { Token } from '@rocket.chat/core-typings'; + import { addAsToken, isToken, validateAllowedTokens } from './token'; -const validateUrl = (url, message) => { +const validateUrl = (url: string, message: { tokens?: Token[] }) => { // Don't render markdown inside links if (message?.tokens?.some((token) => url.includes(token.token))) { return false; @@ -19,20 +21,21 @@ const validateUrl = (url, message) => { } }; -const endsWithWhitespace = (text) => text.substring(text.length - 1).match(/\s/); +const endsWithWhitespace = (text: string) => text.substring(text.length - 1).match(/\s/); -const getParseableMarkersCount = (start, end) => { +const getParseableMarkersCount = (start: string, end: string) => { const usableMarkers = start.length > 1 ? 2 : 1; return end.length - usableMarkers >= 0 ? usableMarkers : 1; }; -const getTextWrapper = (marker, tagName) => (textPrepend, wrappedText, textAppend) => +const getTextWrapper = (marker: string, tagName: string) => (textPrepend: string, wrappedText: string, textAppend: string) => `${textPrepend}${marker}<${tagName}>${wrappedText}${marker}${textAppend}`; -const getRegexReplacer = (replaceFunction, getRegex) => (marker, tagName) => { - const wrapper = getTextWrapper(marker, tagName); - return (msg) => msg.replace(getRegex(marker), (...args) => replaceFunction(wrapper, ...args)); -}; +const getRegexReplacer = + (replaceFunction: (...args: any[]) => string, getRegex: (marker: string) => RegExp) => (marker: string, tagName: string) => { + const wrapper = getTextWrapper(marker, tagName); + return (msg: string) => msg.replace(getRegex(marker), (...args) => replaceFunction(wrapper, ...args)); + }; const getParserWithCustomMarker = getRegexReplacer( (wrapper, match, p1, p2, p3) => { @@ -61,7 +64,10 @@ const parseItalic = getRegexReplacer( () => new RegExp('([^\\r\\n\\s~*_]){0,1}(\\_+(?!\\s))([^\\_\\r\\n]+)(\\_+)([^\\r\\n\\s]){0,1}', 'gm'), )('_', 'em'); -const parseNotEscaped = (message, { supportSchemesForLink, headers, rootUrl }) => { +const parseNotEscaped = ( + message: { msg?: string; html: string; tokens?: Token[] }, + { supportSchemesForLink, headers, rootUrl }: { supportSchemesForLink?: string; headers?: boolean; rootUrl?: string }, +) => { let msg = message.html; if (!message.tokens) { message.tokens = []; @@ -182,7 +188,10 @@ const parseNotEscaped = (message, { supportSchemesForLink, headers, rootUrl }) = return msg; }; -export const markdown = (message, options) => { +export const markdown = ( + message: TMessage, + options: { supportSchemesForLink?: string; headers?: boolean; rootUrl?: string }, +) => { message.html = parseNotEscaped(message, options); return message; }; diff --git a/apps/meteor/app/markdown/lib/parser/original/original.js b/apps/meteor/app/markdown/lib/parser/original/original.js deleted file mode 100644 index c3a7ba160b27d..0000000000000 --- a/apps/meteor/app/markdown/lib/parser/original/original.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Markdown is a named function that will parse markdown syntax - * @param {Object} message - The message object - */ -import { code } from './code.js'; -import { markdown } from './markdown.js'; - -export const original = ( - message, - options = { - supportSchemesForLink: 'http,https', - headers: true, - }, -) => { - // Parse markdown code - message = code(message); - - // Parse markdown - message = markdown(message, options); - - // Replace linebreak to br - message.html = message.html.replace(/\n/gm, '
'); - - return message; -}; diff --git a/apps/meteor/app/markdown/lib/parser/original/original.ts b/apps/meteor/app/markdown/lib/parser/original/original.ts new file mode 100644 index 0000000000000..ede75d71d2cb5 --- /dev/null +++ b/apps/meteor/app/markdown/lib/parser/original/original.ts @@ -0,0 +1,27 @@ +import type { Token } from '@rocket.chat/core-typings'; + +import { code } from './code'; +import { markdown } from './markdown'; + +export const original = ( + message: TMessage, + options: { + supportSchemesForLink?: string; + headers?: boolean; + rootUrl?: string; + } = { + supportSchemesForLink: 'http,https', + headers: true, + }, +): TMessage => { + // Parse markdown code + message = code(message); + + // Parse markdown + message = markdown(message, options); + + // Replace linebreak to br + message.html = message.html.replace(/\n/gm, '
'); + + return message; +}; diff --git a/apps/meteor/app/markdown/lib/parser/original/token.ts b/apps/meteor/app/markdown/lib/parser/original/token.ts index d4b5a4ef8ace6..04b475bf3acc6 100644 --- a/apps/meteor/app/markdown/lib/parser/original/token.ts +++ b/apps/meteor/app/markdown/lib/parser/original/token.ts @@ -1,11 +1,7 @@ -/* - * Markdown is a named function that will parse markdown syntax - * @param {String} msg - The message html - */ import type { IMessage, TokenType, TokenExtra } from '@rocket.chat/core-typings'; import { Random } from '@rocket.chat/random'; -export const addAsToken = (message: IMessage, html: string, type: TokenType, extra?: TokenExtra): string => { +export const addAsToken = (message: Pick, html: string, type: TokenType, extra?: TokenExtra): string => { if (!message.tokens) { message.tokens = []; } @@ -22,7 +18,7 @@ export const addAsToken = (message: IMessage, html: string, type: TokenType, ext export const isToken = (msg: string): boolean => /=!=[.a-z0-9]{17}=!=/gim.test(msg.trim()); -export const validateAllowedTokens = (message: IMessage, id: string, desiredTokens: TokenType[]): boolean => { +export const validateAllowedTokens = (message: Pick, id: string, desiredTokens: TokenType[]): boolean => { const tokens: string[] = id.match(/=!=[.a-z0-9]{17}=!=/gim) || []; const tokensFound = message.tokens?.filter(({ token }) => tokens.includes(token)) || []; return tokensFound.length === 0 || tokensFound.every((token) => token.type && desiredTokens.includes(token.type)); From c47dbdadc076b386cfb13d9a123c870c7c97d6b8 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 26 Jan 2026 22:41:15 -0300 Subject: [PATCH 03/80] refactor: convert `apps/meteor/app/authentication/server/startup/index.js` to TypeScript --- .../2fa/server/code/PasswordCheckFallback.ts | 1 + .../server/startup/{index.js => index.ts} | 199 +++++++++++------- .../classes/converters/UserConverter.ts | 3 +- apps/meteor/app/mailer/server/api.ts | 6 +- .../externals/meteor/accounts-base.d.ts | 34 ++- .../definition/externals/meteor/meteor.d.ts | 7 + apps/meteor/server/lib/callbacks.ts | 2 +- apps/meteor/server/lib/cas/createNewUser.ts | 12 +- apps/meteor/server/lib/compareUserPassword.ts | 1 + .../server/lib/compareUserPasswordHistory.ts | 1 + .../model-typings/src/models/IUsersModel.ts | 2 +- packages/models/src/models/Users.ts | 2 +- 12 files changed, 173 insertions(+), 97 deletions(-) rename apps/meteor/app/authentication/server/startup/{index.js => index.ts} (66%) diff --git a/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts b/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts index ba462abc1f93c..d0b3661677249 100644 --- a/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts +++ b/apps/meteor/app/2fa/server/code/PasswordCheckFallback.ts @@ -1,5 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Accounts } from 'meteor/accounts-base'; +import type { Meteor } from 'meteor/meteor'; import type { ICodeCheck, IProcessInvalidCodeResult } from './ICodeCheck'; import { settings } from '../../../settings/server'; diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.ts similarity index 66% rename from apps/meteor/app/authentication/server/startup/index.js rename to apps/meteor/app/authentication/server/startup/index.ts index 6c23092761b07..c3f579c90e094 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.ts @@ -1,5 +1,6 @@ import { Apps, AppEvents } from '@rocket.chat/apps'; import { User } from '@rocket.chat/core-services'; +import { UserStatus, type IUser } from '@rocket.chat/core-typings'; import { Roles, Settings, Users } from '@rocket.chat/models'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { getLoginExpirationInDays } from '@rocket.chat/tools'; @@ -24,6 +25,7 @@ import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListen import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { getBaseUserFields } from '../../../utils/server/functions/getBaseUserFields'; +import type { ILoginAttempt } from '../ILoginAttempt'; import { isValidAttemptByUser, isValidLoginAttemptByIp } from '../lib/restrictLoginAttempts'; Accounts.config({ @@ -39,22 +41,22 @@ Accounts.config({ * * we are removing the status here because meteor send 'offline' */ -Object.assign(Accounts._defaultPublishFields.projection, (({ status, ...rest }) => rest)(getBaseUserFields(true))); +Object.assign(Accounts._defaultPublishFields.projection, (({ status: _, ...rest }) => rest)(getBaseUserFields(true))); Meteor.startup(() => { settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => { - Accounts._options.loginExpirationInDays = getLoginExpirationInDays(settings.get('Accounts_LoginExpiration')); + Accounts._options.loginExpirationInDays = getLoginExpirationInDays(settings.get('Accounts_LoginExpiration')); - Accounts.emailTemplates.siteName = settings.get('Site_Name'); + Accounts.emailTemplates.siteName = settings.get('Site_Name'); - Accounts.emailTemplates.from = `${settings.get('Site_Name')} <${settings.get('From_Email')}>`; + Accounts.emailTemplates.from = `${settings.get('Site_Name')} <${settings.get('From_Email')}>`; }); }); Accounts.emailTemplates.userToActivate = { subject() { const subject = i18n.t('Accounts_Admin_Email_Approval_Needed_Subject_Default'); - const siteName = settings.get('Site_Name'); + const siteName = settings.get('Site_Name'); return `[${siteName}] ${subject}`; }, @@ -65,9 +67,9 @@ Accounts.emailTemplates.userToActivate = { : 'Accounts_Admin_Email_Approval_Needed_Default'; return Mailer.replace(i18n.t(email), { - name: escapeHTML(options.name), - email: escapeHTML(options.email), - reason: escapeHTML(options.reason), + name: escapeHTML(options.name ?? ''), + email: escapeHTML(options.email ?? ''), + reason: escapeHTML(options.reason ?? ''), }); }, }; @@ -77,7 +79,7 @@ Accounts.emailTemplates.userActivated = { const activated = username ? 'Activated' : 'Approved'; const action = active ? activated : 'Deactivated'; const subject = `Accounts_Email_${action}_Subject`; - const siteName = settings.get('Site_Name'); + const siteName = settings.get('Site_Name'); return `[${siteName}] ${i18n.t(subject)}`; }, @@ -108,13 +110,13 @@ Meteor.startup(() => { }); Accounts.emailTemplates.verifyEmail.html = function (userModel, url) { - const name = safeHtmlDots(userModel.name); + const name = safeHtmlDots((userModel as IUser & { name: string }).name); return Mailer.replace(verifyEmailTemplate, { Verification_Url: url, name }); }; Accounts.emailTemplates.verifyEmail.subject = function () { - const subject = settings.get('Verification_Email_Subject'); + const subject = settings.get('Verification_Email_Subject'); return Mailer.replace(subject || ''); }; @@ -123,15 +125,15 @@ Accounts.urls.resetPassword = function (token) { }; Accounts.emailTemplates.resetPassword.subject = function (userModel) { - return Mailer.replace(settings.get('Forgot_Password_Email_Subject') || '', { - name: userModel.name, + return Mailer.replace(settings.get('Forgot_Password_Email_Subject') || '', { + name: (userModel as IUser & { name: string }).name, }); }; Accounts.emailTemplates.resetPassword.html = function (userModel, url) { return Mailer.replacekey( Mailer.replace(resetPasswordTemplate, { - name: userModel.name, + name: (userModel as IUser & { name: string }).name, }), 'Forgot_Password_Url', url, @@ -139,55 +141,85 @@ Accounts.emailTemplates.resetPassword.html = function (userModel, url) { }; Accounts.emailTemplates.enrollAccount.subject = function (user) { - const subject = settings.get('Accounts_Enrollment_Email_Subject'); + const subject = settings.get('Accounts_Enrollment_Email_Subject'); return Mailer.replace(subject, user); }; -Accounts.emailTemplates.enrollAccount.html = function (user = {} /* , url*/) { +Accounts.emailTemplates.enrollAccount.html = function (user) { return Mailer.replace(enrollAccountTemplate, { - name: escapeHTML(user.name), - email: user.emails && user.emails[0] && escapeHTML(user.emails[0].address), + name: escapeHTML((user as IUser).name ?? ''), + email: user.emails?.[0] && escapeHTML(user.emails[0].address), }); }; -const getLinkedInName = ({ firstName, lastName }) => { - const { preferredLocale, localized: firstNameLocalized } = firstName; - const { localized: lastNameLocalized } = lastName; - - // LinkedIn new format - if (preferredLocale && firstNameLocalized && preferredLocale.language && preferredLocale.country) { - const locale = `${preferredLocale.language}_${preferredLocale.country}`; - - if (firstNameLocalized[locale] && lastNameLocalized[locale]) { - return `${firstNameLocalized[locale]} ${lastNameLocalized[locale]}`; - } - if (firstNameLocalized[locale]) { - return firstNameLocalized[locale]; +type LinkedInName = + | { + firstName: { + preferredLocale?: { + language: string; + country: string; + }; + localized: { + [locale: string]: string; + }; + }; + lastName: { + preferredLocale?: { + language: string; + country: string; + }; + localized: { + [locale: string]: string; + }; + }; + } + | { + firstName: string; + lastName: string; + }; + +const getLinkedInName = ({ firstName, lastName }: LinkedInName): string => { + // Check if it's the old format (simple strings) + if (typeof firstName === 'string' && typeof lastName === 'string') { + return lastName ? `${firstName} ${lastName}` : firstName; + } + + // LinkedIn new format (objects with localized data) + if (typeof firstName === 'object' && typeof lastName === 'object') { + const { preferredLocale, localized: firstNameLocalized } = firstName; + const { localized: lastNameLocalized } = lastName; + + if (preferredLocale && firstNameLocalized && preferredLocale.language && preferredLocale.country) { + const locale = `${preferredLocale.language}_${preferredLocale.country}`; + + if (firstNameLocalized[locale] && lastNameLocalized[locale]) { + return `${firstNameLocalized[locale]} ${lastNameLocalized[locale]}`; + } + if (firstNameLocalized[locale]) { + return firstNameLocalized[locale]; + } } } - // LinkedIn old format - if (!lastName) { - return firstName; - } - return `${firstName} ${lastName}`; + // Fallback: return empty string or first available value + return typeof firstName === 'string' ? firstName : ''; }; -const validateEmailDomain = (user) => { - if (user.type === 'visitor') { +const validateEmailDomain = (user: Meteor.User) => { + if ((user as IUser).type === 'visitor') { return true; } - let domainWhiteList = settings.get('Accounts_AllowedDomainsList'); + const domainWhiteList = settings.get('Accounts_AllowedDomainsList'); if (_.isEmpty(domainWhiteList?.trim())) { return true; } - domainWhiteList = domainWhiteList.split(',').map((domain) => domain.trim()); + const domainWhiteListArray = domainWhiteList.split(',').map((domain) => domain.trim()); if (user.emails && user.emails.length > 0) { const email = user.emails[0].address; - const inWhiteList = domainWhiteList.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); + const inWhiteList = domainWhiteListArray.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); if (!inWhiteList) { throw new Meteor.Error('error-invalid-domain'); @@ -197,15 +229,15 @@ const validateEmailDomain = (user) => { return true; }; -const onCreateUserAsync = async function (options, user = {}) { +const onCreateUserAsync = async function (options: any, user: Meteor.User) { if (!options.skipBeforeCreateUserCallback) { await beforeCreateUserCallback.run(options, user); } - user.status = 'offline'; + user.status = UserStatus.OFFLINE; - user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); - user.inactiveReason = settings.get('Accounts_ManuallyApproveNewUsers') && !user.active ? 'pending_approval' : undefined; + user.active = user.active !== undefined ? user.active : !settings.get('Accounts_ManuallyApproveNewUsers'); + user.inactiveReason = settings.get('Accounts_ManuallyApproveNewUsers') && !user.active ? 'pending_approval' : undefined; if (!user.name) { if (options.profile) { @@ -219,9 +251,9 @@ const onCreateUserAsync = async function (options, user = {}) { } if (user.services) { - const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); + const verified = settings.get('Accounts_Verify_Email_For_External_Accounts'); - for (const service of Object.values(user.services)) { + for (const service of Object.values(user.services as Meteor.UserServices)) { if (!user.name) { user.name = service.name || service.username; } @@ -238,7 +270,7 @@ const onCreateUserAsync = async function (options, user = {}) { } if (!options.skipAdminEmail && !user.active) { - const destinations = []; + const destinations: string[] = []; const usersInRole = await Roles.findUsersInRole('admin'); await usersInRole.forEach((adminUser) => { if (Array.isArray(adminUser.emails)) { @@ -250,12 +282,12 @@ const onCreateUserAsync = async function (options, user = {}) { const email = { to: destinations, - from: settings.get('From_Email'), + from: settings.get('From_Email'), subject: Accounts.emailTemplates.userToActivate.subject(), html: Accounts.emailTemplates.userToActivate.html({ ...options, name: options.name || options.profile?.name, - email: options.email || user.emails[0].address, + email: options.email || user.emails?.[0].address, }), }; @@ -273,7 +305,7 @@ const onCreateUserAsync = async function (options, user = {}) { return user; }; -Accounts.onCreateUser(function (...args) { +Accounts.onCreateUser(function (this: typeof Accounts, ...args) { // Depends on meteor support for Async return onCreateUserAsync.call(this, ...args); }); @@ -281,7 +313,7 @@ Accounts.onCreateUser(function (...args) { const { insertUserDoc } = Accounts; Accounts.insertUserDoc = async function (options, user) { - const globalRoles = new Set(); + const globalRoles = new Set(); if (Match.test(options.globalRoles, [String]) && options.globalRoles.length > 0) { options.globalRoles.map((role) => globalRoles.add(role)); @@ -294,14 +326,14 @@ Accounts.insertUserDoc = async function (options, user) { delete user.globalRoles; if (user.services && !user.services.password && !options.skipAuthServiceDefaultRoles) { - const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); + const defaultAuthServiceRoles = parseCSV(settings.get('Accounts_Registration_AuthenticationServices_Default_Roles') || ''); if (defaultAuthServiceRoles.length > 0) { defaultAuthServiceRoles.map((role) => globalRoles.add(role)); } } - const arrayGlobalRoles = [...globalRoles]; + const arrayGlobalRoles: string[] = [...globalRoles]; const roles = options.skipNewUserRolesSetting ? arrayGlobalRoles : getNewUserRoles(arrayGlobalRoles); if (!user.type) { @@ -309,9 +341,9 @@ Accounts.insertUserDoc = async function (options, user) { } if ( - settings.get('Accounts_TwoFactorAuthentication_Enabled') && - settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled') && - settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In') + settings.get('Accounts_TwoFactorAuthentication_Enabled') && + settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled') && + settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In') ) { user.services = user.services || {}; user.services.email2fa = { @@ -327,9 +359,10 @@ Accounts.insertUserDoc = async function (options, user) { const _id = await insertUserDoc.call(Accounts, options, user); - user = await Users.findOne({ + // TODO I believe this find is unnecessary + user = (await Users.findOne({ _id, - }); + })) as Meteor.User & { globalRoles?: string[] }; /** * if settings shows setup wizard to be pending @@ -342,7 +375,7 @@ Accounts.insertUserDoc = async function (options, user) { const hasAdmin = await Users.findOneByRolesAndType('admin', 'user', { projection: { _id: 1 } }); if (!roles.includes('admin') && !hasAdmin) { roles.push('admin'); - if (settings.get('Show_Setup_Wizard') === 'pending') { + if (settings.get<'pending' | 'in_progress' | 'completed'>('Show_Setup_Wizard') === 'pending') { // TODO: audit (await Settings.updateValueById('Show_Setup_Wizard', 'in_progress')).modifiedCount && void notifyOnSettingChangedById('Show_Setup_Wizard'); @@ -353,7 +386,7 @@ Accounts.insertUserDoc = async function (options, user) { await addUserRolesAsync(_id, roles); // Make user's roles to be present on callback - user = await Users.findOneById(_id, { projection: { username: 1, type: 1, roles: 1 } }); + user = (await Users.findOneById(_id, { projection: { username: 1, type: 1, roles: 1 } })) as Meteor.User & { globalRoles?: string[] }; if (user.username) { if (options.joinDefaultChannels !== false) { @@ -365,8 +398,8 @@ Accounts.insertUserDoc = async function (options, user) { return callbacks.run('afterCreateUser', user); }); } - if (!options.skipDefaultAvatar && settings.get('Accounts_SetDefaultAvatar') === true) { - const avatarSuggestions = await getAvatarSuggestionForUser(user); + if (!options.skipDefaultAvatar && settings.get('Accounts_SetDefaultAvatar') === true) { + const avatarSuggestions = await getAvatarSuggestionForUser(user as IUser); for await (const service of Object.keys(avatarSuggestions)) { const avatarData = avatarSuggestions[service]; if (service !== 'gravatar') { @@ -387,7 +420,7 @@ Accounts.insertUserDoc = async function (options, user) { return _id; }; -const validateLoginAttemptAsync = async function (login) { +const validateLoginAttemptAsync = async function (login: ILoginAttempt) { login = await callbacks.run('beforeValidateLogin', login); if (!(await isValidLoginAttemptByIp(getClientAddress(login.connection)))) { @@ -406,17 +439,17 @@ const validateLoginAttemptAsync = async function (login) { return login.allowed; } - if (login.user.type === 'visitor') { + if (login.user?.type === 'visitor') { return true; } - if (login.user.type === 'app') { + if (login.user?.type === 'app') { throw new Meteor.Error('error-app-user-is-not-allowed-to-login', 'App user is not allowed to login', { function: 'Accounts.validateLoginAttempt', }); } - if (!!login.user.active !== true) { + if (!!login.user?.active !== true) { throw new Meteor.Error('error-user-is-not-activated', 'User is not activated', { function: 'Accounts.validateLoginAttempt', }); @@ -428,8 +461,12 @@ const validateLoginAttemptAsync = async function (login) { }); } - if (login.user.roles.includes('admin') === false && login.type === 'password' && settings.get('Accounts_EmailVerification') === true) { - const validEmail = login.user.emails.filter((email) => email.verified === true); + if ( + login.user.roles.includes('admin') === false && + login.type === 'password' && + settings.get('Accounts_EmailVerification') === true + ) { + const validEmail = login.user.emails?.filter((email) => email.verified === true) ?? []; if (validEmail.length === 0) { throw new Meteor.Error('error-invalid-email', 'Invalid email __email__'); } @@ -437,7 +474,7 @@ const validateLoginAttemptAsync = async function (login) { login = await callbacks.run('onValidateLogin', login); - await Users.updateLastLoginById(login.user._id); + await Users.updateLastLoginById(login.user!._id); setImmediate(() => { return callbacks.run('afterValidateLogin', login); }); @@ -454,20 +491,20 @@ const validateLoginAttemptAsync = async function (login) { return true; }; -Accounts.validateLoginAttempt(function (...args) { +Accounts.validateLoginAttempt(function (this: typeof Accounts, ...args: [ILoginAttempt]) { // Depends on meteor support for Async return validateLoginAttemptAsync.call(this, ...args); }); -Accounts.validateNewUser((user) => { +Accounts.validateNewUser((user: Meteor.User) => { if (user.type === 'visitor') { return true; } if ( - settings.get('Accounts_Registration_AuthenticationServices_Enabled') === false && - settings.get('LDAP_Enable') === false && - !(user.services && user.services.password) + settings.get('Accounts_Registration_AuthenticationServices_Enabled') === false && + settings.get('LDAP_Enable') === false && + !user.services?.password ) { throw new Meteor.Error('registration-disabled-authentication-services', 'User registration is disabled for authentication services'); } @@ -475,21 +512,21 @@ Accounts.validateNewUser((user) => { return true; }); -Accounts.validateNewUser((user) => { +Accounts.validateNewUser((user: Meteor.User) => { if (user.type === 'visitor') { return true; } - let domainWhiteList = settings.get('Accounts_AllowedDomainsList'); + const domainWhiteList = settings.get('Accounts_AllowedDomainsList'); if (_.isEmpty(domainWhiteList?.trim())) { return true; } - domainWhiteList = domainWhiteList.split(',').map((domain) => domain.trim()); + const domainWhiteListArray = domainWhiteList.split(',').map((domain) => domain.trim()); if (user.emails && user.emails.length > 0) { const email = user.emails[0].address; - const inWhiteList = domainWhiteList.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); + const inWhiteList = domainWhiteListArray.some((domain) => email.match(`@${escapeRegExp(domain)}$`)); if (inWhiteList === false) { throw new Meteor.Error('error-invalid-domain'); @@ -499,8 +536,8 @@ Accounts.validateNewUser((user) => { return true; }); -Accounts.onLogin(async ({ user }) => { - if (!user || !user.services || !user.services.resume || !user.services.resume.loginTokens || !user._id) { +Accounts.onLogin(async ({ user }: { user: Meteor.User }) => { + if (!user?.services?.resume?.loginTokens || !user._id) { return; } diff --git a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts index efa721a518ddd..24a49fc504ed9 100644 --- a/apps/meteor/app/importer/server/classes/converters/UserConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/UserConverter.ts @@ -4,6 +4,7 @@ import { Random } from '@rocket.chat/random'; import { SHA256 } from '@rocket.chat/sha256'; import { hash as bcryptHash } from 'bcrypt'; import { Accounts } from 'meteor/accounts-base'; +import type { Meteor } from 'meteor/meteor'; import { RecordConverter, type RecordConverterOptions } from './RecordConverter'; import { generateTempPassword } from './generateTempPassword'; @@ -410,7 +411,7 @@ export class UserConverter extends RecordConverter), ...(userData.roles?.length ? { globalRoles: userData.roles } : {}), }, ); diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index 8fc98f884cb5c..22da574e425a7 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -31,7 +31,7 @@ export const replacekey = (str: string, key: string, value = ''): string => export const translate = (str: string): string => replaceVariables(str, (_match, key) => i18n.t(key, { lng })); -export const replace = (str: string, data: { [key: string]: unknown } = {}): string => { +export const replace = (str: string, data: Record = {}): string => { if (!str) { return ''; } @@ -54,14 +54,14 @@ export const replace = (str: string, data: { [key: string]: unknown } = {}): str const nonEscapeKeys = ['room_path']; -export const replaceEscaped = (str: string, data: { [key: string]: unknown } = {}): string => { +export const replaceEscaped = (str: string, data: Record = {}): string => { const siteName = settings.get('Site_Name'); const siteUrl = settings.get('Site_Url'); return replace(str, { Site_Name: siteName ? escapeHTML(siteName) : undefined, Site_Url: siteUrl ? escapeHTML(siteUrl) : undefined, - ...Object.entries(data).reduce<{ [key: string]: string }>((ret, [key, value]) => { + ...Object.entries(data).reduce>((ret, [key, value]) => { if (value !== undefined && value !== null) { ret[key] = nonEscapeKeys.includes(key) ? String(value) : escapeHTML(String(value)); } diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 875b3cb5291e6..6582ea36c8358 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -1,4 +1,29 @@ +import type { Meteor } from 'meteor/meteor'; + declare module 'meteor/accounts-base' { + type UserToActivateOptions = { + name?: string; + email?: string; + reason?: string; + }; + + type UserActivatedOptions = { + active: boolean; + username?: string; + name: string; + }; + + interface EmailTemplates { + userToActivate: { + subject(options?: UserToActivateOptions): string; + html(options?: UserToActivateOptions): string; + }; + userActivated: { + subject(options: UserActivatedOptions): string; + html(options: UserActivatedOptions): string; + }; + } + namespace Accounts { const storageLocation: Window['localStorage']; function createUser( @@ -17,7 +42,7 @@ declare module 'meteor/accounts-base' { function _getLoginToken(connectionId: string): string | undefined; - function insertUserDoc(options: Record, user: Record): string; + function insertUserDoc(options: Record, user: Partial & { globalRoles?: string[] }): Promise; function _generateStampedLoginToken(): { token: string; when: Date }; @@ -60,7 +85,8 @@ declare module 'meteor/accounts-base' { interface AccountsServerOptions { ambiguousErrorMessages?: boolean; restrictCreationByEmailDomain?: string | (() => string); - forbidClientAccountCreation?: boolean | undefined; + forbidClientAccountCreation?: boolean; + loginExpirationInDays?: number; } export const _options: AccountsServerOptions; @@ -80,5 +106,9 @@ declare module 'meteor/accounts-base' { const connection: { userId(): string | null; }; + + const _defaultPublishFields: { + projection: Record; + }; } } diff --git a/apps/meteor/definition/externals/meteor/meteor.d.ts b/apps/meteor/definition/externals/meteor/meteor.d.ts index d1c40d8065f54..f6216d00ecc57 100644 --- a/apps/meteor/definition/externals/meteor/meteor.d.ts +++ b/apps/meteor/definition/externals/meteor/meteor.d.ts @@ -1,4 +1,5 @@ import 'meteor/meteor'; +import type { IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import type { DDPCommon, IStreamerConstructor, IStreamer } from 'meteor/ddp-common'; @@ -35,6 +36,12 @@ declare module 'meteor/meteor' { reason?: string; } + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface User extends IUser {} + + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface UserServices extends NonNullable {} + interface Device { isDesktop: () => boolean; } diff --git a/apps/meteor/server/lib/callbacks.ts b/apps/meteor/server/lib/callbacks.ts index d9fa18e0491b7..080bbd7a474c7 100644 --- a/apps/meteor/server/lib/callbacks.ts +++ b/apps/meteor/server/lib/callbacks.ts @@ -66,7 +66,6 @@ interface EventLikeCallbackSignatures { 'beforeJoinDefaultChannels': (user: IUser) => void; 'beforeCreateChannel': (owner: IUser, room: IRoom) => void; 'afterCreateRoom': (owner: IUser, room: IRoom) => void; - 'onValidateLogin': (login: ILoginAttempt) => void; 'federation.afterCreateFederatedRoom': ( room: IRoom, second: { @@ -184,6 +183,7 @@ type ChainedCallbackSignatures = { { room, topic, user }: { room: IRoom; topic: string; user: Pick }, ) => void; 'livechat.beforeInquiry': (data: IOmnichannelInquiryExtraData) => Partial; + 'onValidateLogin': (login: ILoginAttempt) => ILoginAttempt; }; export type Hook = diff --git a/apps/meteor/server/lib/cas/createNewUser.ts b/apps/meteor/server/lib/cas/createNewUser.ts index 853fa26c308ca..b595f37ba4286 100644 --- a/apps/meteor/server/lib/cas/createNewUser.ts +++ b/apps/meteor/server/lib/cas/createNewUser.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; -import { pick } from '@rocket.chat/tools'; +import { isTruthy, pick } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; import { logger } from './logger'; @@ -18,12 +18,10 @@ export const createNewUser = async (username: string, { attributes, casVersion, username: attributes.username || username, active: true, globalRoles: ['user'], - emails: [attributes.email] - .filter((e) => e) - .map((address) => ({ - address, - verified: flagEmailAsVerified, - })), + emails: [attributes.email].filter(isTruthy).map((address) => ({ + address, + verified: flagEmailAsVerified, + })), services: { cas: { external_id: username, diff --git a/apps/meteor/server/lib/compareUserPassword.ts b/apps/meteor/server/lib/compareUserPassword.ts index e34294f014ed5..315a20758d8f7 100644 --- a/apps/meteor/server/lib/compareUserPassword.ts +++ b/apps/meteor/server/lib/compareUserPassword.ts @@ -1,5 +1,6 @@ import type { IUser, IPassword } from '@rocket.chat/core-typings'; import { Accounts } from 'meteor/accounts-base'; +import type { Meteor } from 'meteor/meteor'; /** * Check if a given password is the one user by given user or if the user doesn't have a password diff --git a/apps/meteor/server/lib/compareUserPasswordHistory.ts b/apps/meteor/server/lib/compareUserPasswordHistory.ts index 468d76511ec3e..eda527ded1e81 100644 --- a/apps/meteor/server/lib/compareUserPasswordHistory.ts +++ b/apps/meteor/server/lib/compareUserPasswordHistory.ts @@ -1,5 +1,6 @@ import type { IUser, IPassword } from '@rocket.chat/core-typings'; import { Accounts } from 'meteor/accounts-base'; +import type { Meteor } from 'meteor/meteor'; import { settings } from '../../app/settings/server'; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 836a96bf8a9d8..070c13d2e3f65 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -340,7 +340,7 @@ export interface IUsersModel extends IBaseModel { findOneByIdAndLoginToken(userId: string, loginToken: string, options?: FindOptions): Promise; findOneActiveById(userId: string, options?: FindOptions): Promise; findOneByIdOrUsername(userId: string, options?: FindOptions): Promise; - findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions): Promise; + findOneByRolesAndType(roles: IRole['_id'], type: string, options?: FindOptions): Promise; findNotOfflineByIds(userIds: string[], options?: FindOptions): FindCursor; findUsersNotOffline(options?: FindOptions): FindCursor; countUsersNotOffline(options?: FindOptions): Promise; diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index c19417fbc5301..1e00bfe7ef557 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -2442,7 +2442,7 @@ export class UsersRaw extends BaseRaw> implements IU return this.findOne(query, options); } - findOneByRolesAndType(roles: IRole['_id'][], type: string, options?: FindOptions) { + findOneByRolesAndType(roles: IRole['_id'], type: string, options?: FindOptions) { const query = { roles, type }; return this.findOne(query, options); From 3fc4b0fc9ab2aa8d41418f419b1172a8d4ddfb29 Mon Sep 17 00:00:00 2001 From: Tasso Date: Tue, 27 Jan 2026 17:57:59 -0300 Subject: [PATCH 04/80] refactor: convert `apps/meteor/app/2fa/server/MethodInvocationOverride.js` to TypeScript --- .../2fa/server/MethodInvocationOverride.js | 17 ------------ .../2fa/server/MethodInvocationOverride.ts | 26 +++++++++++++++++++ apps/meteor/app/api/server/ApiClass.ts | 2 +- .../externals/meteor/ddp-common.d.ts | 9 ++++--- .../definition/externals/meteor/ddp.d.ts | 5 ++-- 5 files changed, 35 insertions(+), 24 deletions(-) delete mode 100644 apps/meteor/app/2fa/server/MethodInvocationOverride.js create mode 100644 apps/meteor/app/2fa/server/MethodInvocationOverride.ts diff --git a/apps/meteor/app/2fa/server/MethodInvocationOverride.js b/apps/meteor/app/2fa/server/MethodInvocationOverride.js deleted file mode 100644 index e176402019102..0000000000000 --- a/apps/meteor/app/2fa/server/MethodInvocationOverride.js +++ /dev/null @@ -1,17 +0,0 @@ -import { DDP } from 'meteor/ddp'; -import { DDPCommon } from 'meteor/ddp-common'; - -class MethodInvocation extends DDPCommon.MethodInvocation { - constructor(options) { - const result = super(options); - const currentInvocation = DDP._CurrentInvocation.get(); - - if (currentInvocation) { - this.twoFactorChecked = currentInvocation.twoFactorChecked; - } - - return result; - } -} - -DDPCommon.MethodInvocation = MethodInvocation; diff --git a/apps/meteor/app/2fa/server/MethodInvocationOverride.ts b/apps/meteor/app/2fa/server/MethodInvocationOverride.ts new file mode 100644 index 0000000000000..a1713131233b3 --- /dev/null +++ b/apps/meteor/app/2fa/server/MethodInvocationOverride.ts @@ -0,0 +1,26 @@ +import { DDP } from 'meteor/ddp'; +import { DDPCommon } from 'meteor/ddp-common'; + +class MethodInvocation extends DDPCommon.MethodInvocation { + twoFactorChecked?: boolean; + + constructor(options: { + connection: { + id: string; + close: () => void; + clientAddress: string; + httpHeaders: Record; + }; + isSimulation?: boolean; + userId?: string; + }) { + super(options); + const currentInvocation = DDP._CurrentInvocation.get(); + + if (currentInvocation) { + this.twoFactorChecked = (currentInvocation as MethodInvocation).twoFactorChecked; + } + } +} + +DDPCommon.MethodInvocation = MethodInvocation; diff --git a/apps/meteor/app/api/server/ApiClass.ts b/apps/meteor/app/api/server/ApiClass.ts index b68ba93e9bf71..e87d7f442665b 100644 --- a/apps/meteor/app/api/server/ApiClass.ts +++ b/apps/meteor/app/api/server/ApiClass.ts @@ -1088,7 +1088,7 @@ export class APIClass Meteor.callAsync('login', args)); + const auth = await DDP._CurrentInvocation.withValue(invocation, async () => Meteor.callAsync('login', args)); this.user = await Users.findOne( { _id: auth.id, diff --git a/apps/meteor/definition/externals/meteor/ddp-common.d.ts b/apps/meteor/definition/externals/meteor/ddp-common.d.ts index cbe96a9722968..d0015b821f6bd 100644 --- a/apps/meteor/definition/externals/meteor/ddp-common.d.ts +++ b/apps/meteor/definition/externals/meteor/ddp-common.d.ts @@ -2,8 +2,9 @@ declare module 'meteor/ddp-common' { namespace DDPCommon { function stringifyDDP(msg: EJSONable): string; function parseDDP(msg: string): EJSONable; - class MethodInvocation { - constructor(options: { + + interface MethodInvocationConstructor { + new (options: { connection: { id: string; close: () => void; @@ -12,9 +13,11 @@ declare module 'meteor/ddp-common' { }; isSimulation?: boolean; userId?: string; - }); + }): MethodInvocation; } + let MethodInvocation: MethodInvocationConstructor; + /** * Heartbeat options */ diff --git a/apps/meteor/definition/externals/meteor/ddp.d.ts b/apps/meteor/definition/externals/meteor/ddp.d.ts index 48df3a6a458b3..2aec429596685 100644 --- a/apps/meteor/definition/externals/meteor/ddp.d.ts +++ b/apps/meteor/definition/externals/meteor/ddp.d.ts @@ -1,9 +1,8 @@ import type { DDPCommon } from 'meteor/ddp-common'; +import type { Meteor } from 'meteor/meteor'; declare module 'meteor/ddp' { namespace DDP { - const _CurrentInvocation: { - withValue(invocation: DDPCommon.MethodInvocation, func: () => Promise): Promise; - }; + const _CurrentInvocation: Meteor.EnvironmentVariable; } } From 2ee66f7fec5b37e3d971c41aa0e86a0871d170c7 Mon Sep 17 00:00:00 2001 From: Tasso Date: Tue, 27 Jan 2026 22:19:17 -0300 Subject: [PATCH 05/80] refactor: convert `apps/meteor/app/apps/server/bridges/bridges.js` to TypeScript --- .../server/bridges/{bridges.js => bridges.ts} | 141 ++++++++++++++---- .../ee/server/apps/communication/rest.ts | 1 + apps/meteor/ee/server/apps/orchestrator.js | 3 + .../apps-engine/src/server/bridges/index.ts | 86 ++++------- 4 files changed, 144 insertions(+), 87 deletions(-) rename apps/meteor/app/apps/server/bridges/{bridges.js => bridges.ts} (54%) diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.ts similarity index 54% rename from apps/meteor/app/apps/server/bridges/bridges.js rename to apps/meteor/app/apps/server/bridges/bridges.ts index 3b49cd91394e9..ec8db6e4368e6 100644 --- a/apps/meteor/app/apps/server/bridges/bridges.js +++ b/apps/meteor/app/apps/server/bridges/bridges.ts @@ -1,4 +1,31 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import { AppBridges } from '@rocket.chat/apps-engine/server/bridges'; +import type { + ApiBridge, + CloudWorkspaceBridge, + CommandBridge, + ContactBridge, + EmailBridge, + EnvironmentalVariableBridge, + ExperimentalBridge, + HttpBridge, + IInternalBridge, + IInternalFederationBridge, + LivechatBridge, + MessageBridge, + ModerationBridge, + OutboundMessageBridge, + PersistenceBridge, + RoleBridge, + RoomBridge, + SchedulerBridge, + ServerSettingBridge, + ThreadBridge, + UserBridge, + VideoConferenceBridge, +} from '@rocket.chat/apps-engine/server/bridges'; +import type { OAuthAppsBridge } from '@rocket.chat/apps-engine/server/bridges/OAuthAppsBridge'; +import type { UploadBridge } from '@rocket.chat/apps-engine/server/bridges/UploadBridge'; import { AppActivationBridge } from './activation'; import { AppApisBridge } from './api'; @@ -30,7 +57,63 @@ import { AppUserBridge } from './users'; import { AppVideoConferenceBridge } from './videoConferences'; export class RealAppBridges extends AppBridges { - constructor(orch) { + private _actBridge: AppActivationBridge; + + private _cmdBridge: AppCommandsBridge; + + private _apiBridge: AppApisBridge; + + private _detBridge: AppDetailChangesBridge; + + private _envBridge: AppEnvironmentalVariableBridge; + + private _httpBridge: AppHttpBridge; + + private _lisnBridge: AppListenerBridge; + + private _msgBridge: AppMessageBridge; + + private _persistBridge: AppPersistenceBridge; + + private _roomBridge: AppRoomBridge; + + private _internalBridge: AppInternalBridge; + + private _setsBridge: AppSettingBridge; + + private _userBridge: AppUserBridge; + + private _livechatBridge: AppLivechatBridge; + + private _uploadBridge: AppUploadBridge; + + private _uiInteractionBridge: UiInteractionBridge; + + private _schedulerBridge: AppSchedulerBridge; + + private _cloudWorkspaceBridge: AppCloudBridge; + + private _videoConfBridge: AppVideoConferenceBridge; + + private _oAuthBridge: AppOAuthAppsBridge; + + private _internalFedBridge: AppInternalFederationBridge; + + private _moderationBridge: AppModerationBridge; + + private _threadBridge: AppThreadBridge; + + private _roleBridge: AppRoleBridge; + + private _emailBridge: AppEmailBridge; + + private _contactBridge: AppContactBridge; + + private _outboundMessageBridge: OutboundCommunicationBridge; + + private _experimentalBridge: AppExperimentalBridge; + + constructor(orch: IAppServerOrchestrator) { super(); this._actBridge = new AppActivationBridge(orch); @@ -63,115 +146,115 @@ export class RealAppBridges extends AppBridges { this._experimentalBridge = new AppExperimentalBridge(orch); } - getCommandBridge() { + getCommandBridge(): CommandBridge { return this._cmdBridge; } - getApiBridge() { + getApiBridge(): ApiBridge { return this._apiBridge; } - getEnvironmentalVariableBridge() { + getEnvironmentalVariableBridge(): EnvironmentalVariableBridge { return this._envBridge; } - getHttpBridge() { + getHttpBridge(): HttpBridge { return this._httpBridge; } getListenerBridge() { - return this._lisnBridge; + return this._lisnBridge as any; // FIXME: AppListenerBridge does not implement IListenerBridge } - getMessageBridge() { + getMessageBridge(): MessageBridge { return this._msgBridge; } - getThreadBridge() { + getThreadBridge(): ThreadBridge { return this._threadBridge; } - getPersistenceBridge() { + getPersistenceBridge(): PersistenceBridge { return this._persistBridge; } - getAppActivationBridge() { + getAppActivationBridge(): AppActivationBridge { return this._actBridge; } - getAppDetailChangesBridge() { + getAppDetailChangesBridge(): AppDetailChangesBridge { return this._detBridge; } - getRoomBridge() { + getRoomBridge(): RoomBridge { return this._roomBridge; } - getInternalBridge() { + getInternalBridge(): IInternalBridge { return this._internalBridge; } - getServerSettingBridge() { + getServerSettingBridge(): ServerSettingBridge { return this._setsBridge; } - getUserBridge() { + getUserBridge(): UserBridge { return this._userBridge; } - getLivechatBridge() { + getLivechatBridge(): LivechatBridge { return this._livechatBridge; } - getUploadBridge() { + getUploadBridge(): UploadBridge { return this._uploadBridge; } - getUiInteractionBridge() { + getUiInteractionBridge(): UiInteractionBridge { return this._uiInteractionBridge; } - getSchedulerBridge() { + getSchedulerBridge(): SchedulerBridge { return this._schedulerBridge; } - getCloudWorkspaceBridge() { + getCloudWorkspaceBridge(): CloudWorkspaceBridge { return this._cloudWorkspaceBridge; } - getVideoConferenceBridge() { + getVideoConferenceBridge(): VideoConferenceBridge { return this._videoConfBridge; } - getOutboundMessageBridge() { + getOutboundMessageBridge(): OutboundMessageBridge { return this._outboundMessageBridge; } - getOAuthAppsBridge() { + getOAuthAppsBridge(): OAuthAppsBridge { return this._oAuthBridge; } - getInternalFederationBridge() { + getInternalFederationBridge(): IInternalFederationBridge { return this._internalFedBridge; } - getModerationBridge() { + getModerationBridge(): ModerationBridge { return this._moderationBridge; } - getRoleBridge() { + getRoleBridge(): RoleBridge { return this._roleBridge; } - getEmailBridge() { + getEmailBridge(): EmailBridge { return this._emailBridge; } - getContactBridge() { + getContactBridge(): ContactBridge { return this._contactBridge; } - getExperimentalBridge() { + getExperimentalBridge(): ExperimentalBridge { return this._experimentalBridge; } } diff --git a/apps/meteor/ee/server/apps/communication/rest.ts b/apps/meteor/ee/server/apps/communication/rest.ts index dfc2abddda55c..89dbd049c081c 100644 --- a/apps/meteor/ee/server/apps/communication/rest.ts +++ b/apps/meteor/ee/server/apps/communication/rest.ts @@ -508,6 +508,7 @@ export class AppsRestApi { try { const { event, externalComponent } = this.bodyParams; + // FIXME this fails since there is no implementation for externalComponentEvent in ListenerBridge const result = (Apps.getBridges()?.getListenerBridge() as Record).externalComponentEvent(event, externalComponent); return API.v1.success({ result }); diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.js index 7188695ac3f43..d2ed01062ce1c 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.js @@ -371,6 +371,9 @@ export class AppServerOrchestrator { return this._manager.updateAppsMarketplaceInfo(apps).then(() => this._manager.get()); } + /** + * @returns {Promise} + */ async installedApps(filter = {}) { if (!this.isLoaded()) { return; diff --git a/packages/apps-engine/src/server/bridges/index.ts b/packages/apps-engine/src/server/bridges/index.ts index c472ad2293acb..9ee6b3fcf09c8 100644 --- a/packages/apps-engine/src/server/bridges/index.ts +++ b/packages/apps-engine/src/server/bridges/index.ts @@ -1,58 +1,28 @@ -import { ApiBridge } from './ApiBridge'; -import { AppActivationBridge } from './AppActivationBridge'; -import { AppBridges } from './AppBridges'; -import { AppDetailChangesBridge } from './AppDetailChangesBridge'; -import { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; -import { CommandBridge } from './CommandBridge'; -import { ContactBridge } from './ContactBridge'; -import { EmailBridge } from './EmailBridge'; -import { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; -import { ExperimentalBridge } from './ExperimentalBridge'; -import { HttpBridge, IHttpBridgeRequestInfo } from './HttpBridge'; -import { IInternalBridge } from './IInternalBridge'; -import { IInternalFederationBridge } from './IInternalFederationBridge'; -import { IListenerBridge } from './IListenerBridge'; -import { LivechatBridge } from './LivechatBridge'; -import { MessageBridge } from './MessageBridge'; -import { ModerationBridge } from './ModerationBridge'; -import { OutboundMessageBridge } from './OutboundMessagesBridge'; -import { PersistenceBridge } from './PersistenceBridge'; -import { RoleBridge } from './RoleBridge'; -import { RoomBridge } from './RoomBridge'; -import { SchedulerBridge } from './SchedulerBridge'; -import { ServerSettingBridge } from './ServerSettingBridge'; -import { UiInteractionBridge } from './UiInteractionBridge'; -import { UploadBridge } from './UploadBridge'; -import { UserBridge } from './UserBridge'; -import { VideoConferenceBridge } from './VideoConferenceBridge'; - -export { - CloudWorkspaceBridge, - ContactBridge, - EnvironmentalVariableBridge, - HttpBridge, - IHttpBridgeRequestInfo, - IListenerBridge, - LivechatBridge, - MessageBridge, - PersistenceBridge, - AppActivationBridge, - AppDetailChangesBridge, - CommandBridge, - ApiBridge, - RoomBridge, - IInternalBridge, - ServerSettingBridge, - UserBridge, - UploadBridge, - EmailBridge, - ExperimentalBridge, - UiInteractionBridge, - SchedulerBridge, - AppBridges, - VideoConferenceBridge, - IInternalFederationBridge, - ModerationBridge, - RoleBridge, - OutboundMessageBridge, -}; +export { ApiBridge } from './ApiBridge'; +export { AppActivationBridge } from './AppActivationBridge'; +export { AppBridges } from './AppBridges'; +export { AppDetailChangesBridge } from './AppDetailChangesBridge'; +export { CloudWorkspaceBridge } from './CloudWorkspaceBridge'; +export { CommandBridge } from './CommandBridge'; +export { ContactBridge } from './ContactBridge'; +export { EmailBridge } from './EmailBridge'; +export { EnvironmentalVariableBridge } from './EnvironmentalVariableBridge'; +export { ExperimentalBridge } from './ExperimentalBridge'; +export { HttpBridge, IHttpBridgeRequestInfo } from './HttpBridge'; +export { IInternalBridge } from './IInternalBridge'; +export { IInternalFederationBridge } from './IInternalFederationBridge'; +export { IListenerBridge } from './IListenerBridge'; +export { LivechatBridge } from './LivechatBridge'; +export { MessageBridge } from './MessageBridge'; +export { ModerationBridge } from './ModerationBridge'; +export { OutboundMessageBridge } from './OutboundMessagesBridge'; +export { PersistenceBridge } from './PersistenceBridge'; +export { RoleBridge } from './RoleBridge'; +export { RoomBridge } from './RoomBridge'; +export { SchedulerBridge } from './SchedulerBridge'; +export { ServerSettingBridge } from './ServerSettingBridge'; +export { ThreadBridge } from './ThreadBridge'; +export { UiInteractionBridge } from './UiInteractionBridge'; +export { UploadBridge } from './UploadBridge'; +export { UserBridge } from './UserBridge'; +export { VideoConferenceBridge } from './VideoConferenceBridge'; From 14dcb40ccad958c2bb2a4ca244fcc09f544159fd Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 02:20:30 -0300 Subject: [PATCH 06/80] refactor: convert `apps/meteor/app/apps/server/converters/departments.js` to TypeScript --- .../server/converters/departments.spec.ts | 260 ++++++++++++++++++ .../{departments.js => departments.ts} | 38 ++- .../src/definition/livechat/IDepartment.ts | 2 +- 3 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 apps/meteor/app/apps/server/converters/departments.spec.ts rename apps/meteor/app/apps/server/converters/{departments.js => departments.ts} (52%) diff --git a/apps/meteor/app/apps/server/converters/departments.spec.ts b/apps/meteor/app/apps/server/converters/departments.spec.ts new file mode 100644 index 0000000000000..55dd6d987c9c0 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/departments.spec.ts @@ -0,0 +1,260 @@ +import type { IAppServerOrchestrator, IAppsDepartment } from '@rocket.chat/apps'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; +import { LivechatDepartment } from '@rocket.chat/models'; + +import { AppDepartmentsConverter } from './departments'; + +jest.mock('@rocket.chat/models', () => ({ + LivechatDepartment: { + findOneById: jest.fn(), + }, +})); + +describe('AppDepartmentsConverter', () => { + let converter: AppDepartmentsConverter; + + beforeEach(() => { + converter = new AppDepartmentsConverter({} as IAppServerOrchestrator); + jest.clearAllMocks(); + }); + + describe('convertById', () => { + it('should convert a department by id', async () => { + const mockDepartment: ILivechatDepartment = { + _id: 'dept1', + name: 'Sales', + email: 'sales@example.com', + _updatedAt: new Date('2023-01-01'), + enabled: true, + numAgents: 5, + showOnOfflineForm: true, + showOnRegistration: true, + description: 'Sales department', + offlineMessageChannelName: 'sales-offline', + requestTagBeforeClosingChat: false, + chatClosingTags: ['resolved'], + abandonedRoomsCloseCustomMessage: 'Goodbye', + waitingQueueMessage: 'Please wait', + departmentsAllowedToForward: ['dept2'], + }; + + (LivechatDepartment.findOneById as jest.Mock).mockResolvedValue(mockDepartment); + + const result = await converter.convertById('dept1'); + + expect(LivechatDepartment.findOneById).toHaveBeenCalledWith('dept1'); + expect(result).toMatchObject({ + id: 'dept1', + name: 'Sales', + email: 'sales@example.com', + updatedAt: new Date('2023-01-01'), + enabled: true, + numberOfAgents: 5, + showOnOfflineForm: true, + showOnRegistration: true, + description: 'Sales department', + offlineMessageChannelName: 'sales-offline', + requestTagBeforeClosingChat: false, + chatClosingTags: ['resolved'], + abandonedRoomsCloseCustomMessage: 'Goodbye', + waitingQueueMessage: 'Please wait', + departmentsAllowedToForward: ['dept2'], + }); + }); + + it('should return undefined when department is not found', async () => { + (LivechatDepartment.findOneById as jest.Mock).mockResolvedValue(null); + + const result = await converter.convertById('nonexistent'); + + expect(result).toBeUndefined(); + }); + }); + + describe('convertDepartment', () => { + it('should return undefined when department is null', async () => { + const result = await converter.convertDepartment(null); + expect(result).toBeUndefined(); + }); + + it('should return undefined when department is undefined', async () => { + const result = await converter.convertDepartment(undefined); + expect(result).toBeUndefined(); + }); + + it('should convert a department with all fields', async () => { + const mockDepartment: ILivechatDepartment = { + _id: 'dept1', + name: 'Support', + email: 'support@example.com', + _updatedAt: new Date('2023-02-01'), + enabled: true, + numAgents: 10, + showOnOfflineForm: false, + showOnRegistration: false, + description: 'Support department', + offlineMessageChannelName: 'support-offline', + requestTagBeforeClosingChat: true, + chatClosingTags: ['closed', 'resolved'], + abandonedRoomsCloseCustomMessage: 'Chat closed', + waitingQueueMessage: 'Waiting...', + departmentsAllowedToForward: ['dept2', 'dept3'], + }; + + const result = await converter.convertDepartment(mockDepartment); + + expect(result).toMatchObject({ + id: 'dept1', + name: 'Support', + email: 'support@example.com', + updatedAt: new Date('2023-02-01'), + enabled: true, + numberOfAgents: 10, + showOnOfflineForm: false, + showOnRegistration: false, + description: 'Support department', + offlineMessageChannelName: 'support-offline', + requestTagBeforeClosingChat: true, + chatClosingTags: ['closed', 'resolved'], + abandonedRoomsCloseCustomMessage: 'Chat closed', + waitingQueueMessage: 'Waiting...', + departmentsAllowedToForward: ['dept2', 'dept3'], + }); + }); + + it('should convert a department with minimal fields', async () => { + const mockDepartment: ILivechatDepartment = { + _id: 'dept2', + name: 'Minimal', + email: 'minimal@example.com', + _updatedAt: new Date('2023-03-01'), + enabled: false, + numAgents: 1, + showOnOfflineForm: true, + showOnRegistration: true, + offlineMessageChannelName: 'minimal-offline', + }; + + const result = await converter.convertDepartment(mockDepartment); + + expect(result).toMatchObject({ + id: 'dept2', + name: 'Minimal', + email: 'minimal@example.com', + updatedAt: new Date('2023-03-01'), + enabled: false, + numberOfAgents: 1, + showOnOfflineForm: true, + showOnRegistration: true, + }); + }); + }); + + describe('convertAppDepartment', () => { + it('should return undefined when department is null', () => { + const result = converter.convertAppDepartment(null); + expect(result).toBeUndefined(); + }); + + it('should return undefined when department is undefined', () => { + const result = converter.convertAppDepartment(undefined); + expect(result).toBeUndefined(); + }); + + it('should convert an app department to livechat department', () => { + const appDepartment: IAppsDepartment = { + id: 'dept1', + name: 'Sales', + email: 'sales@example.com', + updatedAt: new Date('2023-01-01'), + enabled: true, + numberOfAgents: 5, + showOnOfflineForm: true, + showOnRegistration: true, + description: 'Sales department', + offlineMessageChannelName: 'sales-offline', + requestTagBeforeClosingChat: false, + chatClosingTags: ['resolved'], + abandonedRoomsCloseCustomMessage: 'Goodbye', + waitingQueueMessage: 'Please wait', + departmentsAllowedToForward: ['dept2'], + }; + + const result = converter.convertAppDepartment(appDepartment); + + expect(result).toEqual({ + _id: 'dept1', + name: 'Sales', + email: 'sales@example.com', + _updatedAt: new Date('2023-01-01'), + enabled: true, + numAgents: 5, + showOnOfflineForm: true, + showOnRegistration: true, + description: 'Sales department', + offlineMessageChannelName: 'sales-offline', + requestTagBeforeClosingChat: false, + chatClosingTags: ['resolved'], + abandonedRoomsCloseCustomMessage: 'Goodbye', + waitingQueueMessage: 'Please wait', + departmentsAllowedToForward: ['dept2'], + }); + }); + + it('should handle unmapped properties', () => { + const appDepartment: IAppsDepartment & { _unmappedProperties_?: Record } = { + id: 'dept2', + name: 'Support', + email: 'support@example.com', + updatedAt: new Date('2023-02-01'), + enabled: true, + numberOfAgents: 10, + showOnOfflineForm: false, + showOnRegistration: false, + offlineMessageChannelName: 'support-offline', + _unmappedProperties_: { + customField1: 'value1', + customField2: 'value2', + }, + }; + + const result = converter.convertAppDepartment(appDepartment); + + expect(result).toMatchObject({ + _id: 'dept2', + name: 'Support', + email: 'support@example.com', + customField1: 'value1', + customField2: 'value2', + }); + }); + + it('should convert an app department with minimal fields', () => { + const appDepartment: IAppsDepartment = { + id: 'dept3', + name: 'Minimal', + email: 'minimal@example.com', + updatedAt: new Date('2023-03-01'), + enabled: false, + numberOfAgents: 1, + showOnOfflineForm: true, + showOnRegistration: true, + offlineMessageChannelName: 'minimal-offline', + }; + + const result = converter.convertAppDepartment(appDepartment); + + expect(result).toEqual({ + _id: 'dept3', + name: 'Minimal', + email: 'minimal@example.com', + _updatedAt: new Date('2023-03-01'), + enabled: false, + numAgents: 1, + showOnOfflineForm: true, + showOnRegistration: true, + offlineMessageChannelName: 'minimal-offline', + }); + }); + }); +}); diff --git a/apps/meteor/app/apps/server/converters/departments.js b/apps/meteor/app/apps/server/converters/departments.ts similarity index 52% rename from apps/meteor/app/apps/server/converters/departments.js rename to apps/meteor/app/apps/server/converters/departments.ts index 3fbfd07e9e99b..0a465f4fba263 100644 --- a/apps/meteor/app/apps/server/converters/departments.js +++ b/apps/meteor/app/apps/server/converters/departments.ts @@ -1,19 +1,25 @@ +import type { IAppDepartmentsConverter, IAppsDepartment, IAppServerOrchestrator } from '@rocket.chat/apps'; +import type { ILivechatDepartment } from '@rocket.chat/core-typings'; import { LivechatDepartment } from '@rocket.chat/models'; import { transformMappedData } from './transformMappedData'; -export class AppDepartmentsConverter { - constructor(orch) { - this.orch = orch; - } +export class AppDepartmentsConverter implements IAppDepartmentsConverter { + constructor(public orch: IAppServerOrchestrator) {} - async convertById(id) { - const department = await LivechatDepartment.findOneById(id); + async convertById(departmentId: ILivechatDepartment['_id']): Promise { + const department = await LivechatDepartment.findOneById(departmentId); return this.convertDepartment(department); } - async convertDepartment(department) { + convertDepartment(department: undefined | null): Promise; + + convertDepartment(department: ILivechatDepartment): Promise; + + convertDepartment(department: ILivechatDepartment | undefined | null): Promise; + + async convertDepartment(department: ILivechatDepartment | undefined | null): Promise { if (!department) { return undefined; } @@ -39,22 +45,28 @@ export class AppDepartmentsConverter { return transformMappedData(department, map); } - convertAppDepartment(department) { + convertAppDepartment(department: undefined | null): undefined; + + convertAppDepartment(department: IAppsDepartment): ILivechatDepartment; + + convertAppDepartment(department: IAppsDepartment | undefined | null): ILivechatDepartment | undefined; + + convertAppDepartment(department: IAppsDepartment | undefined | null): ILivechatDepartment | undefined { if (!department) { return undefined; } - const newDepartment = { + const newDepartment: ILivechatDepartment = { _id: department.id, - name: department.name, - email: department.email, + name: department.name ?? '', + email: department.email ?? '', _updatedAt: department.updatedAt, enabled: department.enabled, numAgents: department.numberOfAgents, showOnOfflineForm: department.showOnOfflineForm, showOnRegistration: department.showOnRegistration, description: department.description, - offlineMessageChannelName: department.offlineMessageChannelName, + offlineMessageChannelName: department.offlineMessageChannelName ?? '', requestTagBeforeClosingChat: department.requestTagBeforeClosingChat, chatClosingTags: department.chatClosingTags, abandonedRoomsCloseCustomMessage: department.abandonedRoomsCloseCustomMessage, @@ -62,6 +74,6 @@ export class AppDepartmentsConverter { departmentsAllowedToForward: department.departmentsAllowedToForward, }; - return Object.assign(newDepartment, department._unmappedProperties_); + return Object.assign(newDepartment, (department as { _unmappedProperties_?: Record })._unmappedProperties_); } } diff --git a/packages/apps-engine/src/definition/livechat/IDepartment.ts b/packages/apps-engine/src/definition/livechat/IDepartment.ts index f8b92f3d8b077..6e7190404b4c9 100644 --- a/packages/apps-engine/src/definition/livechat/IDepartment.ts +++ b/packages/apps-engine/src/definition/livechat/IDepartment.ts @@ -8,7 +8,7 @@ export interface IDepartment { chatClosingTags?: Array; abandonedRoomsCloseCustomMessage?: string; waitingQueueMessage?: string; - departmentsAllowedToForward?: string; + departmentsAllowedToForward?: string[]; enabled: boolean; updatedAt: Date; numberOfAgents: number; From 9b4c2b94f31e4e381370f7ccab8dc8d05486b207 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 02:45:01 -0300 Subject: [PATCH 07/80] refactor: convert `apps/meteor/app/apps/server/converters/messages.js` to TypeScript --- .../converters/{messages.js => messages.ts} | 132 ++++++++++-------- 1 file changed, 77 insertions(+), 55 deletions(-) rename apps/meteor/app/apps/server/converters/{messages.js => messages.ts} (62%) diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.ts similarity index 62% rename from apps/meteor/app/apps/server/converters/messages.js rename to apps/meteor/app/apps/server/converters/messages.ts index a824df3228396..d9c0f6b3548d7 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.ts @@ -1,25 +1,31 @@ +import type { IAppMessagesConverter, IAppServerOrchestrator, IAppsMessage, IAppsMesssageRaw } from '@rocket.chat/apps'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; +import type { IMessage, MessageAttachment, MessageQuoteAttachment } from '@rocket.chat/core-typings'; import { isMessageFromVisitor } from '@rocket.chat/core-typings'; import { Messages, Rooms, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { removeEmpty } from '@rocket.chat/tools'; +import type * as UiKit from '@rocket.chat/ui-kit'; import { cachedFunction } from './cachedFunction'; import { transformMappedData } from './transformMappedData'; -export class AppMessagesConverter { +export class AppMessagesConverter implements IAppMessagesConverter { mem = new WeakMap(); - constructor(orch) { - this.orch = orch; - } + constructor(public orch: IAppServerOrchestrator) {} - async convertById(msgId) { - const msg = await Messages.findOneById(msgId); + async convertById(messageId: IMessage['_id']): Promise { + const msg = await Messages.findOneById(messageId); return this.convertMessage(msg); } - async convertMessageRaw(msgObj) { + convertMessageRaw(message: IMessage): Promise; + + convertMessageRaw(message: IMessage | undefined | null): Promise; + + async convertMessageRaw(msgObj: IMessage | undefined | null): Promise { if (!msgObj) { return undefined; } @@ -55,14 +61,22 @@ export class AppMessagesConverter { return transformMappedData(message, map); } - async convertMessage(msgObj, cacheObj = msgObj) { + convertMessage(message: undefined | null): Promise; + + convertMessage(message: IMessage): Promise; + + convertMessage(message: IMessage, cacheObj?: object): Promise; + + convertMessage(message: IMessage | undefined | null): Promise; + + async convertMessage(msgObj: IMessage | undefined | null, cacheObj?: object): Promise { if (!msgObj) { return undefined; } const cache = - this.mem.get(cacheObj) ?? - new Map([ + this.mem.get(cacheObj ?? msgObj) ?? + new Map any>([ ['room', cachedFunction(this.orch.getConverters().get('rooms').convertById.bind(this.orch.getConverters().get('rooms')))], [ 'user.convertById', @@ -74,7 +88,7 @@ export class AppMessagesConverter { ], ]); - this.mem.set(cacheObj, cache); + this.mem.set(cacheObj ?? msgObj, cache); const map = { id: '_id', @@ -94,14 +108,14 @@ export class AppMessagesConverter { token: 'token', blocks: 'blocks', type: 't', - room: async (message) => { + room: async (message: IMessage) => { const result = await cache.get('room')(message.rid); - delete message.rid; + delete (message as Partial).rid; // FIXME ??? return result; }, - editor: async (message) => { - const { editedBy } = message; - delete message.editedBy; + editor: async (message: IMessage) => { + const { editedBy } = message as { editedBy?: { _id: string } }; // FIXME ??? + delete (message as { editedBy?: unknown }).editedBy; // FIXME ??? if (!editedBy) { return undefined; @@ -109,13 +123,13 @@ export class AppMessagesConverter { return cache.get('user.convertById')(editedBy._id); }, - attachments: async (message) => { + attachments: async (message: IMessage) => { const result = await this._convertAttachmentsToApp(message.attachments); delete message.attachments; return result; }, - sender: async (message) => { - if (!message.u || !message.u._id) { + sender: async (message: IMessage) => { + if (!message.u?._id) { return undefined; } @@ -124,7 +138,7 @@ export class AppMessagesConverter { ? cache.get('user.convertToApp')(message.u) : cache.get('user.convertById')(message.u._id)); - delete message.u; + delete (message as any).u; // FIXME the property is used right after, so we can't delete it before, what??? /** * Old System Messages from visitor doesn't have the `token` field, to not return @@ -138,7 +152,15 @@ export class AppMessagesConverter { return transformMappedData(msgObj, map); } - async convertAppMessage(message, isPartial = false) { + convertAppMessage(message: undefined | null): Promise; + + convertAppMessage(message: IAppsMessage): Promise; + + convertAppMessage(message: IAppsMessage | undefined | null): Promise; + + convertAppMessage(message: IAppsMessage, isPartial: boolean): Promise>; + + async convertAppMessage(message: IAppsMessage | undefined | null, isPartial = false): Promise | undefined> { if (!message) { return undefined; } @@ -160,7 +182,7 @@ export class AppMessagesConverter { if (user) { u = { _id: user._id, - username: user.username, + username: user.username!, name: user.name, }; } else { @@ -176,8 +198,8 @@ export class AppMessagesConverter { if (message.editor) { const editor = await Users.findOneById(message.editor.id); editedBy = { - _id: editor._id, - username: editor.username, + _id: editor!._id, + username: editor!.username, }; } @@ -196,14 +218,14 @@ export class AppMessagesConverter { } } - const newMessage = { - _id, + const newMessage: IMessage = { + _id: _id!, ...('threadId' in message && { tmid: message.threadId }), - rid, - u, - msg: message.text, - ts, - _updatedAt: message.updatedAt, + rid: rid!, + u: u!, + msg: message.text!, + ts: ts!, + _updatedAt: message.updatedAt!, ...(editedBy && { editedBy }), ...('editedAt' in message && { editedAt: message.editedAt }), ...('emoji' in message && { emoji: message.emoji }), @@ -212,26 +234,26 @@ export class AppMessagesConverter { ...('customFields' in message && { customFields: message.customFields }), ...('groupable' in message && { groupable: message.groupable }), ...(attachments && { attachments }), - ...('reactions' in message && { reactions: message.reactions }), + ...('reactions' in message && { reactions: message.reactions as IMessage['reactions'] }), ...('parseUrls' in message && { parseUrls: message.parseUrls }), - ...('blocks' in message && { blocks: message.blocks }), - ...('token' in message && { token: message.token }), + ...('blocks' in message && { blocks: message.blocks as UiKit.MessageSurfaceLayout | undefined }), + ...('token' in message && { token: message.token as IMessage['token'] }), }; if (isPartial) { Object.entries(newMessage).forEach(([key, value]) => { if (typeof value === 'undefined') { - delete newMessage[key]; + delete newMessage[key as keyof typeof newMessage]; } }); } else { - Object.assign(newMessage, message._unmappedProperties_); + Object.assign(newMessage, (message as { _unmappedProperties_?: any })._unmappedProperties_); } return newMessage; } - _convertAppAttachments(attachments) { + private _convertAppAttachments(attachments: IMessageAttachment[] | undefined) { if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { return undefined; } @@ -250,28 +272,28 @@ export class AppMessagesConverter { title: attachment.title ? attachment.title.value : undefined, title_link: attachment.title ? attachment.title.link : undefined, title_link_download: attachment.title ? attachment.title.displayDownloadLink : undefined, - image_dimensions: attachment.imageDimensions, - image_preview: attachment.imagePreview, + image_dimensions: (attachment as { imageDimensions?: unknown }).imageDimensions, + image_preview: (attachment as { imagePreview?: unknown }).imagePreview, image_url: attachment.imageUrl, - image_type: attachment.imageType, - image_size: attachment.imageSize, + image_type: (attachment as { imageType?: unknown }).imageType, + image_size: (attachment as { imageSize?: unknown }).imageSize, audio_url: attachment.audioUrl, - audio_type: attachment.audioType, - audio_size: attachment.audioSize, + audio_type: (attachment as { audioType?: unknown }).audioType, + audio_size: (attachment as { audioSize?: unknown }).audioSize, video_url: attachment.videoUrl, - video_type: attachment.videoType, - video_size: attachment.videoSize, + video_type: (attachment as { videoType?: unknown }).videoType, + video_size: (attachment as { videoSize?: unknown }).videoSize, fields: attachment.fields, button_alignment: attachment.actionButtonsAlignment, actions: attachment.actions, type: attachment.type, description: attachment.description, - ...attachment._unmappedProperties_, + ...(attachment as { _unmappedProperties_?: any })._unmappedProperties_, }), ); } - async _convertAttachmentsToApp(attachments) { + private async _convertAttachmentsToApp(attachments: MessageAttachment[] | undefined) { if (typeof attachments === 'undefined' || !Array.isArray(attachments)) { return undefined; } @@ -298,16 +320,16 @@ export class AppMessagesConverter { actions: 'actions', type: 'type', description: 'description', - author: (attachment) => { - const { author_name: name, author_link: link, author_icon: icon } = attachment; + author: (attachment: MessageAttachment) => { + const { author_name: name, author_link: link, author_icon: icon } = attachment as MessageQuoteAttachment; - delete attachment.author_name; - delete attachment.author_link; - delete attachment.author_icon; + delete (attachment as Partial).author_name; + delete (attachment as Partial).author_link; + delete (attachment as Partial).author_icon; return { name, link, icon }; }, - title: (attachment) => { + title: (attachment: MessageAttachment) => { const { title: value, title_link: link, title_link_download: displayDownloadLink } = attachment; delete attachment.title; @@ -316,8 +338,8 @@ export class AppMessagesConverter { return { value, link, displayDownloadLink }; }, - timestamp: (attachment) => { - const result = new Date(attachment.ts); + timestamp: (attachment: MessageAttachment) => { + const result = new Date(attachment.ts!); delete attachment.ts; return result; }, From 3c8ebd2eec685efb574321aaf1fe1fdd2d4e7ced Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 03:11:59 -0300 Subject: [PATCH 08/80] refactor: convert `apps/meteor/app/apps/server/converters/rooms.js` to TypeScript --- .../server/converters/{rooms.js => rooms.ts} | 192 ++++++++++-------- packages/core-typings/src/IRoom.ts | 2 +- 2 files changed, 108 insertions(+), 86 deletions(-) rename apps/meteor/app/apps/server/converters/{rooms.js => rooms.ts} (57%) diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.ts similarity index 57% rename from apps/meteor/app/apps/server/converters/rooms.js rename to apps/meteor/app/apps/server/converters/rooms.ts index c81792a0bb390..9d654d046a932 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.ts @@ -1,34 +1,39 @@ +import type { IAppRoomsConverter, IAppServerOrchestrator, IAppsLivechatRoom, IAppsRoom, IAppsRoomRaw } from '@rocket.chat/apps'; +import type { ILivechatRoom } from '@rocket.chat/apps-engine/definition/livechat'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IOmnichannelRoom, IRoom, IUser } from '@rocket.chat/core-typings'; import { LivechatVisitors, Rooms, LivechatDepartment, Users, LivechatContacts } from '@rocket.chat/models'; import { transformMappedData } from './transformMappedData'; -export class AppRoomsConverter { - constructor(orch) { - this.orch = orch; - } +export class AppRoomsConverter implements IAppRoomsConverter { + constructor(public orch: IAppServerOrchestrator) {} - async convertById(roomId) { + async convertById(roomId: IRoom['_id']): Promise { const room = await Rooms.findOneById(roomId); return this.convertRoom(room); } - async convertByName(roomName) { - const room = await Rooms.findOneByName(roomName); + async convertByName(roomName: IRoom['name']): Promise { + const room = await Rooms.findOneByName(roomName!); return this.convertRoom(room); } - convertRoomRaw(room) { + convertRoomRaw(room: IRoom): Promise; + + convertRoomRaw(room: IRoom | undefined | null): Promise; + + async convertRoomRaw(room: IRoom | undefined | null): Promise { if (!room) { return undefined; } - const mapUserLookup = (user) => + const mapUserLookup = (user: IRoom['u'] & { id?: IUser['_id'] }) => user && { _id: user._id ?? user.id, - ...(user.username && { username: user.username }), + username: user.username!, ...(user.name && { name: user.name }), }; @@ -58,69 +63,70 @@ export class AppRoomsConverter { contactId: 'contactId', departmentId: 'departmentId', parentRoomId: 'prid', - visitor: (data) => { - const { v } = data; + visitor: (data: IRoom) => { + const { v } = data as IOmnichannelRoom; if (!v) { return undefined; } - delete data.v; + delete (data as Partial).v; - const { _id: id, ...rest } = v; + const { _id: id, name, ...rest } = v; return { id, + name: name!, ...rest, }; }, - displaySystemMessages: (data) => { + displaySystemMessages: (data: IRoom) => { const { sysMes } = data; delete data.sysMes; - return typeof sysMes === 'undefined' ? true : sysMes; + return typeof sysMes === 'undefined' ? true : (sysMes as unknown as boolean); // FIXME this conversion is incorrect }, - type: (data) => { + type: (data: IRoom) => { const result = this._convertTypeToApp(data.t); - delete data.t; + delete (data as Partial).t; return result; }, - creator: (data) => { + creator: (data: IRoom) => { if (!data.u) { return undefined; } const creator = mapUserLookup(data.u); - delete data.u; + delete (data as Partial).u; return creator; }, - closedBy: (data) => { - if (!data.closedBy) { + closedBy: (data: IRoom) => { + if (!(data as IOmnichannelRoom).closedBy) { return undefined; } - const { closedBy } = data; - delete data.closedBy; - return mapUserLookup(closedBy); + const { closedBy } = data as IOmnichannelRoom; + delete (data as Partial).closedBy; + return mapUserLookup(closedBy!); }, - servedBy: (data) => { - if (!data.servedBy) { + servedBy: (data: IRoom) => { + if (!(data as IOmnichannelRoom).servedBy) { return undefined; } - const { servedBy } = data; - delete data.servedBy; - return mapUserLookup(servedBy); + const { servedBy } = data as IOmnichannelRoom; + delete (data as Partial).servedBy; + return mapUserLookup(servedBy!); }, - responseBy: (data) => { - if (!data.responseBy) { + responseBy: (data: IRoom) => { + if (!(data as IOmnichannelRoom).responseBy) { return undefined; } - const { responseBy } = data; - delete data.responseBy; - return mapUserLookup(responseBy); + const { responseBy } = data as IOmnichannelRoom; + delete (data as Partial).responseBy; + return mapUserLookup(responseBy!); }, }; return transformMappedData(room, map); } - async __getCreator(user) { + private async __getCreator(user: IUser['_id'] | undefined) { if (!user) { return; } @@ -137,17 +143,17 @@ export class AppRoomsConverter { }; } - async __getVisitor({ visitor: roomVisitor, visitorChannelInfo }) { + private async __getVisitor({ visitor: roomVisitor, visitorChannelInfo }: ILivechatRoom) { if (!roomVisitor) { return; } - const visitor = await LivechatVisitors.findOneEnabledById(roomVisitor.id); + const visitor = await LivechatVisitors.findOneEnabledById(roomVisitor.id!); if (!visitor) { return; } - const { lastMessageTs, phone } = visitorChannelInfo; + const { lastMessageTs, phone } = visitorChannelInfo!; return { _id: visitor._id, @@ -160,7 +166,7 @@ export class AppRoomsConverter { }; } - async __getUserIdAndUsername(userObj) { + private async __getUserIdAndUsername(userObj: ILivechatRoom['servedBy']) { if (!userObj?.id) { return; } @@ -176,7 +182,7 @@ export class AppRoomsConverter { }; } - async __getRoomCloser(room, v) { + private async __getRoomCloser(room: ILivechatRoom, v: Awaited>) { if (!room.closedBy) { return; } @@ -202,7 +208,7 @@ export class AppRoomsConverter { } // TODO do we really need this? - async __getContactId({ contact }) { + private async __getContactId({ contact }: ILivechatRoom) { if (!contact?._id) { return; } @@ -211,7 +217,7 @@ export class AppRoomsConverter { } // TODO do we really need this? - async __getDepartment({ department }) { + private async __getDepartment({ department }: ILivechatRoom) { if (!department) { return; } @@ -219,22 +225,30 @@ export class AppRoomsConverter { return dept?._id; } - async convertAppRoom(room, isPartial = false) { + convertAppRoom(room: undefined | null): Promise; + + convertAppRoom(room: IAppsRoom): Promise; + + convertAppRoom(room: IAppsRoom, isPartial: boolean): Promise>; + + convertAppRoom(room: IAppsRoom | undefined | null, isPartial?: boolean): Promise | undefined>; + + async convertAppRoom(room: IAppsRoom | undefined | null, isPartial = false): Promise | undefined> { if (!room) { return undefined; } const u = await this.__getCreator(room.creator?.id); - const v = await this.__getVisitor(room); + const v = await this.__getVisitor(room as ILivechatRoom); - const departmentId = await this.__getDepartment(room); + const departmentId = await this.__getDepartment(room as ILivechatRoom); - const servedBy = await this.__getUserIdAndUsername(room.servedBy); + const servedBy = await this.__getUserIdAndUsername((room as ILivechatRoom).servedBy); - const closedBy = await this.__getRoomCloser(room, v); + const closedBy = await this.__getRoomCloser(room as ILivechatRoom, v); - const contactId = await this.__getContactId(room); + const contactId = await this.__getContactId(room as ILivechatRoom); const newRoom = { ...(room.id && { _id: room.id }), @@ -244,7 +258,7 @@ export class AppRoomsConverter { ...(typeof room.updatedAt !== 'undefined' && { _updatedAt: room.updatedAt }), ...(room.displayName && { fname: room.displayName }), ...(room.type !== 'd' && room.slugifiedName && { name: room.slugifiedName }), - ...(room.members && { members: room.members }), + ...((room as any).members && { members: (room as any).members }), ...(typeof room.isDefault !== 'undefined' && { default: room.isDefault }), ...(typeof room.isReadOnly !== 'undefined' && { ro: room.isReadOnly }), ...(typeof room.displaySystemMessages !== 'undefined' && { sysMes: room.displaySystemMessages }), @@ -254,30 +268,38 @@ export class AppRoomsConverter { ...(servedBy && { servedBy }), ...(closedBy && { closedBy }), ...(room.userIds && { uids: room.userIds }), - ...(typeof room.isWaitingResponse !== 'undefined' && { waitingResponse: !!room.isWaitingResponse }), - ...(typeof room.isOpen !== 'undefined' && { open: !!room.isOpen }), - ...(room.closedAt && { closedAt: room.closedAt }), + ...(typeof (room as ILivechatRoom).isWaitingResponse !== 'undefined' && { + waitingResponse: !!(room as ILivechatRoom).isWaitingResponse, + }), + ...(typeof (room as ILivechatRoom).isOpen !== 'undefined' && { open: !!(room as ILivechatRoom).isOpen }), + ...((room as ILivechatRoom).closedAt && { closedAt: (room as ILivechatRoom).closedAt }), ...(room.lastModifiedAt && { lm: room.lastModifiedAt }), ...(room.customFields && { customFields: room.customFields }), ...(room.livechatData && { livechatData: room.livechatData }), ...(typeof room.parentRoom !== 'undefined' && { prid: room.parentRoom.id }), ...(contactId && { contactId }), - ...(room._USERNAMES && { _USERNAMES: room._USERNAMES }), - ...(room.source && { + ...((room as { _USERNAMES?: string[] })._USERNAMES && { _USERNAMES: (room as { _USERNAMES?: string[] })._USERNAMES }), + ...((room as ILivechatRoom).source && { source: { - ...room.source, + ...(room as ILivechatRoom).source, }, }), }; if (!isPartial) { - Object.assign(newRoom, room._unmappedProperties_); + Object.assign(newRoom, (room as { _unmappedProperties_?: Record })._unmappedProperties_); } return newRoom; } - async convertRoom(originalRoom) { + convertRoom(room: undefined | null): Promise; + + convertRoom(room: IRoom): Promise; + + convertRoom(room: IRoom | undefined | null): Promise; + + async convertRoom(originalRoom: IRoom | undefined | null): Promise { if (!originalRoom) { return undefined; } @@ -303,17 +325,17 @@ export class AppRoomsConverter { closer: 'closer', teamId: 'teamId', isTeamMain: 'teamMain', - isDefault: (room) => { + isDefault: (room: IRoom) => { const result = !!room.default; delete room.default; return result; }, - isReadOnly: (room) => { + isReadOnly: (room: IRoom) => { const result = !!room.ro; delete room.ro; return result; }, - displaySystemMessages: (room) => { + displaySystemMessages: (room: IRoom) => { const { sysMes } = room; if (typeof sysMes === 'undefined') { @@ -323,24 +345,24 @@ export class AppRoomsConverter { delete room.sysMes; return sysMes; }, - type: (room) => { + type: (room: IRoom) => { const result = this._convertTypeToApp(room.t); - delete room.t; + delete (room as Partial).t; return result; }, - creator: async (room) => { + creator: async (room: IRoom) => { const { u } = room; if (!u) { return undefined; } - delete room.u; + delete (room as Partial).u; return this.orch.getConverters().get('users').convertById(u._id); }, - visitor: (room) => { - const { v } = room; + visitor: (room: IRoom) => { + const { v } = room as IOmnichannelRoom; if (!v) { return undefined; @@ -348,8 +370,8 @@ export class AppRoomsConverter { return this.orch.getConverters().get('visitors').convertById(v._id); }, - contact: (room) => { - const { contactId } = room; + contact: (room: IRoom) => { + const { contactId } = room as IOmnichannelRoom; if (!contactId) { return undefined; @@ -363,8 +385,8 @@ export class AppRoomsConverter { // let's call X and Y. Then if the contact sends a message using X phone number, // then room.v.phoneNo would be X and correspondingly we'll store the timestamp of // the last message from this visitor from X phone no on room.v.lastMessageTs - visitorChannelInfo: (room) => { - const { v } = room; + visitorChannelInfo: (room: IRoom) => { + const { v } = room as IOmnichannelRoom; if (!v) { return undefined; @@ -377,32 +399,32 @@ export class AppRoomsConverter { ...(lastMessageTs && { lastMessageTs }), }; }, - department: async (room) => { - const { departmentId } = room; + department: async (room: IRoom) => { + const { departmentId } = room as IOmnichannelRoom; if (!departmentId) { return undefined; } - delete room.departmentId; + delete (room as Partial).departmentId; return this.orch.getConverters().get('departments').convertById(departmentId); }, - closedBy: async (room) => { - const { closedBy } = room; + closedBy: async (room: IRoom) => { + const { closedBy } = room as IOmnichannelRoom; if (!closedBy) { return undefined; } - delete room.closedBy; - if (originalRoom.closer === 'user') { + delete (room as Partial).closedBy; + if ((originalRoom as IOmnichannelRoom).closer === 'user') { return this.orch.getConverters().get('users').convertById(closedBy._id); } return this.orch.getConverters().get('visitors').convertById(closedBy._id); }, - servedBy: async (room) => { + servedBy: async (room: IRoom) => { const { servedBy } = room; if (!servedBy) { @@ -413,18 +435,18 @@ export class AppRoomsConverter { return this.orch.getConverters().get('users').convertById(servedBy._id); }, - responseBy: async (room) => { - const { responseBy } = room; + responseBy: async (room: IRoom) => { + const { responseBy } = room as IOmnichannelRoom; if (!responseBy) { return undefined; } - delete room.responseBy; + delete (room as Partial).responseBy; return this.orch.getConverters().get('users').convertById(responseBy._id); }, - parentRoom: async (room) => { + parentRoom: async (room: IRoom) => { const { prid } = room; if (!prid) { @@ -437,10 +459,10 @@ export class AppRoomsConverter { }, }; - return transformMappedData(originalRoom, map); + return transformMappedData(originalRoom, map) as unknown as Promise; // FIXME } - _convertTypeToApp(typeChar) { + private _convertTypeToApp(typeChar: IRoom['t']) { switch (typeChar) { case 'c': return RoomType.CHANNEL; diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index fb243a8f92e52..7ee0fb231deb4 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -29,7 +29,7 @@ export interface IRoom extends IRocketChatRecord { reactWhenReadOnly?: boolean; - // TODO: this boolean might be an accident + // TODO: this boolean might be an accident (edit: probably from apps conversion) sysMes?: MessageTypesValues[] | boolean; u: Pick; From 64e1d5f3a19c7207d990c2361644cb1adaa8c661 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 03:16:33 -0300 Subject: [PATCH 09/80] refactor: convert `apps/meteor/app/apps/server/converters/settings.js` to TypeScript --- .../app/apps/server/converters/settings.js | 52 ---- .../apps/server/converters/settings.spec.ts | 281 ++++++++++++++++++ .../app/apps/server/converters/settings.ts | 77 +++++ .../src/definition/settings/ISetting.ts | 2 + packages/core-typings/src/ISetting.ts | 2 +- 5 files changed, 361 insertions(+), 53 deletions(-) delete mode 100644 apps/meteor/app/apps/server/converters/settings.js create mode 100644 apps/meteor/app/apps/server/converters/settings.spec.ts create mode 100644 apps/meteor/app/apps/server/converters/settings.ts diff --git a/apps/meteor/app/apps/server/converters/settings.js b/apps/meteor/app/apps/server/converters/settings.js deleted file mode 100644 index 07b790cb7c592..0000000000000 --- a/apps/meteor/app/apps/server/converters/settings.js +++ /dev/null @@ -1,52 +0,0 @@ -import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; -import { Settings } from '@rocket.chat/models'; - -export class AppSettingsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(settingId) { - const setting = await Settings.findOneById(settingId); - - return this.convertToApp(setting); - } - - convertToApp(setting) { - return { - id: setting._id, - type: this._convertTypeToApp(setting.type), - packageValue: setting.packageValue, - values: setting.values, - value: setting.value, - public: setting.public, - hidden: setting.hidden, - group: setting.group, - i18nLabel: setting.i18nLabel, - i18nDescription: setting.i18nDescription, - createdAt: setting.ts, - updatedAt: setting._updatedAt, - }; - } - - _convertTypeToApp(type) { - switch (type) { - case 'boolean': - return SettingType.BOOLEAN; - case 'code': - return SettingType.CODE; - case 'color': - return SettingType.COLOR; - case 'font': - return SettingType.FONT; - case 'int': - return SettingType.NUMBER; - case 'select': - return SettingType.SELECT; - case 'string': - return SettingType.STRING; - default: - return type; - } - } -} diff --git a/apps/meteor/app/apps/server/converters/settings.spec.ts b/apps/meteor/app/apps/server/converters/settings.spec.ts new file mode 100644 index 0000000000000..9dd2865ce3054 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/settings.spec.ts @@ -0,0 +1,281 @@ +import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import type { ISetting } from '@rocket.chat/core-typings'; + +jest.mock('@rocket.chat/models', () => ({ + Settings: { + findOneById: jest.fn(), + }, +})); + +describe('AppSettingsConverter', () => { + let AppSettingsConverter: any; + let settingsConverter: any; + let mockOrchestrator: any; + let Settings: any; + + beforeAll(async () => { + const module = await import('./settings'); + AppSettingsConverter = module.AppSettingsConverter; + const modelsModule = await import('@rocket.chat/models'); + Settings = modelsModule.Settings; + }); + + beforeEach(() => { + mockOrchestrator = {}; + settingsConverter = new AppSettingsConverter(mockOrchestrator); + jest.clearAllMocks(); + }); + + describe('convertToApp', () => { + it('should convert a boolean setting to app format', () => { + const setting: ISetting = { + _id: 'test-boolean-setting', + type: 'boolean', + packageValue: false, + value: true, + public: true, + hidden: false, + group: 'General', + i18nLabel: 'Test Boolean Setting', + i18nDescription: 'A test boolean setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result).toMatchObject({ + id: 'test-boolean-setting', + type: SettingType.BOOLEAN, + packageValue: false, + value: true, + public: true, + hidden: false, + group: 'General', + i18nLabel: 'Test Boolean Setting', + i18nDescription: 'A test boolean setting', + createdAt: new Date('2024-01-01T00:00:00.000Z'), + updatedAt: new Date('2024-01-02T00:00:00.000Z'), + }); + }); + + it('should convert a string setting to app format', () => { + const setting: ISetting = { + _id: 'test-string-setting', + type: 'string', + packageValue: 'default', + value: 'custom', + public: false, + hidden: true, + group: 'Advanced', + i18nLabel: 'Test String Setting', + i18nDescription: 'A test string setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result).toMatchObject({ + id: 'test-string-setting', + type: SettingType.STRING, + packageValue: 'default', + value: 'custom', + public: false, + hidden: true, + }); + }); + + it('should convert an int setting to NUMBER type', () => { + const setting: ISetting = { + _id: 'test-int-setting', + type: 'int', + packageValue: 100, + value: 200, + public: true, + hidden: false, + group: 'Performance', + i18nLabel: 'Test Int Setting', + i18nDescription: 'A test integer setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result.type).toBe(SettingType.NUMBER); + }); + + it('should convert a select setting with values', () => { + const setting: ISetting = { + _id: 'test-select-setting', + type: 'select', + packageValue: 'option1', + value: 'option2', + values: [ + { key: 'option1', i18nLabel: 'Option 1' }, + { key: 'option2', i18nLabel: 'Option 2' }, + ], + public: true, + hidden: false, + group: 'UI', + i18nLabel: 'Test Select Setting', + i18nDescription: 'A test select setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result.type).toBe(SettingType.SELECT); + expect(result.values).toEqual([ + { key: 'option1', i18nLabel: 'Option 1' }, + { key: 'option2', i18nLabel: 'Option 2' }, + ]); + }); + + it('should convert a code setting to app format', () => { + const setting: ISetting = { + _id: 'test-code-setting', + type: 'code', + packageValue: 'console.log("default");', + value: 'console.log("custom");', + public: false, + hidden: false, + group: 'Custom', + i18nLabel: 'Test Code Setting', + i18nDescription: 'A test code setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result.type).toBe(SettingType.CODE); + }); + + it('should convert a color setting to app format', () => { + const setting: ISetting = { + _id: 'test-color-setting', + type: 'color', + packageValue: '#000000', + value: '#FFFFFF', + public: true, + hidden: false, + group: 'Theme', + i18nLabel: 'Test Color Setting', + i18nDescription: 'A test color setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result.type).toBe(SettingType.COLOR); + }); + + it('should convert a font setting to app format', () => { + const setting: ISetting = { + _id: 'test-font-setting', + type: 'font', + packageValue: 'Arial', + value: 'Helvetica', + public: true, + hidden: false, + group: 'Theme', + i18nLabel: 'Test Font Setting', + i18nDescription: 'A test font setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result.type).toBe(SettingType.FONT); + }); + + it('should pass through unknown types unchanged', () => { + const setting: ISetting = { + _id: 'test-unknown-setting', + type: 'unknown-type' as any, + packageValue: 'value', + value: 'value', + public: true, + hidden: false, + group: 'Other', + i18nLabel: 'Test Unknown Setting', + i18nDescription: 'A test unknown setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + const result = settingsConverter.convertToApp(setting); + + expect(result.type).toBe('unknown-type'); + }); + }); + + describe('convertById', () => { + it('should fetch setting by id and convert it', async () => { + const mockSetting: ISetting = { + _id: 'test-setting-by-id', + type: 'boolean', + packageValue: false, + value: true, + public: true, + hidden: false, + group: 'General', + i18nLabel: 'Test Setting', + i18nDescription: 'A test setting', + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + (Settings.findOneById as jest.Mock).mockResolvedValue(mockSetting); + + const result = await settingsConverter.convertById('test-setting-by-id'); + + expect(Settings.findOneById).toHaveBeenCalledWith('test-setting-by-id'); + expect(result).toMatchObject({ + id: 'test-setting-by-id', + type: SettingType.BOOLEAN, + value: true, + }); + }); + + it('should handle settings without optional fields', async () => { + const mockSetting: ISetting = { + _id: 'minimal-setting', + type: 'string', + value: 'test', + public: false, + hidden: false, + ts: new Date('2024-01-01T00:00:00.000Z'), + _updatedAt: new Date('2024-01-02T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + } as ISetting; + + (Settings.findOneById as jest.Mock).mockResolvedValue(mockSetting); + + const result = await settingsConverter.convertById('minimal-setting'); + + expect(result.id).toBe('minimal-setting'); + expect(result.type).toBe(SettingType.STRING); + }); + + it('should throw an error when setting is not found', async () => { + (Settings.findOneById as jest.Mock).mockResolvedValue(null); + + await expect(settingsConverter.convertById('non-existent-setting')).rejects.toThrow(); + }); + }); +}); diff --git a/apps/meteor/app/apps/server/converters/settings.ts b/apps/meteor/app/apps/server/converters/settings.ts new file mode 100644 index 0000000000000..3a4e68c6991ec --- /dev/null +++ b/apps/meteor/app/apps/server/converters/settings.ts @@ -0,0 +1,77 @@ +import type { IAppServerOrchestrator, IAppSettingsConverter, IAppsSetting } from '@rocket.chat/apps'; +import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; +import type { ISetting } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; + +// import { exhaustiveCheck } from '../../../../lib/utils/exhaustiveCheck'; + +export class AppSettingsConverter implements IAppSettingsConverter { + constructor(public orch: IAppServerOrchestrator) {} + + async convertById(settingId: ISetting['_id']): Promise { + const setting = await Settings.findOneById(settingId); + + if (!setting) { + throw new Error(`Setting with id "${settingId}" not found`); + } + + return this.convertToApp(setting); + } + + convertToApp(setting: ISetting): IAppsSetting { + return { + id: setting._id, + type: this._convertTypeToApp(setting.type), + packageValue: setting.packageValue, + values: setting.values, + value: setting.value, + public: setting.public, + hidden: setting.hidden, + group: setting.group, + i18nLabel: setting.i18nLabel, + i18nDescription: setting.i18nDescription, + createdAt: setting.ts, + updatedAt: setting._updatedAt, + required: false, + }; + } + + private _convertTypeToApp(type: ISetting['type']): SettingType { + switch (type) { + case 'boolean': + return SettingType.BOOLEAN; + case 'code': + return SettingType.CODE; + case 'color': + return SettingType.COLOR; + case 'font': + return SettingType.FONT; + case 'int': + return SettingType.NUMBER; + case 'select': + return SettingType.SELECT; + case 'string': + return SettingType.STRING; + case 'multiSelect': + return SettingType.MULTI_SELECT; + case 'password': + return SettingType.PASSWORD; + case 'roomPick': + return SettingType.ROOM_PICK; + case 'group': + case 'action': + case 'asset': + case 'timezone': + case 'relativeUrl': + case 'language': + case 'date': + case 'lookup': + case 'range': + case 'timespan': + return type as SettingType; // FIXME probably wrong but matches the previous behavior + default: + // exhaustiveCheck(type); + return type as SettingType; // FIXME probably wrong but matches the previous behavior + } + } +} diff --git a/packages/apps-engine/src/definition/settings/ISetting.ts b/packages/apps-engine/src/definition/settings/ISetting.ts index fc529590305a6..1d907d1baa458 100644 --- a/packages/apps-engine/src/definition/settings/ISetting.ts +++ b/packages/apps-engine/src/definition/settings/ISetting.ts @@ -21,6 +21,8 @@ export interface ISetting { public: boolean; /** Whether this setting should be hidden from the user/administrator's eyes (can't be hidden and required). */ hidden?: boolean; + /** The group this setting belongs to. */ + group?: string; /** The selectable values when the setting's type is "select" or "multiSelect". */ values?: Array; /** Whether the **string** type is several lines or just one line. */ diff --git a/packages/core-typings/src/ISetting.ts b/packages/core-typings/src/ISetting.ts index fbc1b031bce66..9dfcfd4acb7ad 100644 --- a/packages/core-typings/src/ISetting.ts +++ b/packages/core-typings/src/ISetting.ts @@ -20,7 +20,7 @@ export type SettingValue = | null; export interface ISettingSelectOption { - key: string | number; + key: string; i18nLabel: string; } From 22c688c606f80e00f2851e008b3e8da045140c51 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 03:30:16 -0300 Subject: [PATCH 10/80] refactor: convert `apps/meteor/app/apps/server/converters/uploads.js` to TypeScript --- .../app/apps/server/converters/uploads.js | 98 --------------- .../app/apps/server/converters/uploads.ts | 114 ++++++++++++++++++ 2 files changed, 114 insertions(+), 98 deletions(-) delete mode 100644 apps/meteor/app/apps/server/converters/uploads.js create mode 100644 apps/meteor/app/apps/server/converters/uploads.ts diff --git a/apps/meteor/app/apps/server/converters/uploads.js b/apps/meteor/app/apps/server/converters/uploads.js deleted file mode 100644 index 60f85a8aa72f1..0000000000000 --- a/apps/meteor/app/apps/server/converters/uploads.js +++ /dev/null @@ -1,98 +0,0 @@ -import { Uploads } from '@rocket.chat/models'; - -import { transformMappedData } from './transformMappedData'; - -export class AppUploadsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(id) { - const upload = await Uploads.findOneById(id); - - return this.convertToApp(upload); - } - - async convertToApp(upload) { - if (!upload) { - return undefined; - } - - const map = { - id: '_id', - name: 'name', - size: 'size', - type: 'type', - store: 'store', - description: 'description', - complete: 'complete', - uploading: 'uploading', - extension: 'extension', - progress: 'progress', - etag: 'etag', - path: 'path', - token: 'token', - url: 'url', - updatedAt: '_updatedAt', - uploadedAt: 'uploadedAt', - room: async (upload) => { - const result = await this.orch.getConverters().get('rooms').convertById(upload.rid); - delete upload.rid; - return result; - }, - user: async (upload) => { - if (!upload.userId) { - return undefined; - } - - const result = await this.orch.getConverters().get('users').convertById(upload.userId); - delete upload.userId; - return result; - }, - visitor: async (upload) => { - if (!upload.visitorToken) { - return undefined; - } - - const result = await this.orch.getConverters().get('visitors').convertByToken(upload.visitorToken); - delete upload.visitorToken; - return result; - }, - }; - - return transformMappedData(upload, map); - } - - convertToRocketChat(upload) { - if (!upload) { - return undefined; - } - - const { id: userId } = upload.user || {}; - const { token: visitorToken } = upload.visitor || {}; - const { id: rid } = upload.room; - - const newUpload = { - _id: upload.id, - name: upload.name, - size: upload.size, - type: upload.type, - extension: upload.extension, - description: upload.description, - store: upload.store, - etag: upload.etag, - complete: upload.complete, - uploading: upload.uploading, - progress: upload.progress, - token: upload.token, - url: upload.url, - _updatedAt: upload.updatedAt, - uploadedAt: upload.uploadedAt, - rid, - userId, - visitorToken, - }; - - return Object.assign(newUpload, upload._unmappedProperties_); - } -} diff --git a/apps/meteor/app/apps/server/converters/uploads.ts b/apps/meteor/app/apps/server/converters/uploads.ts new file mode 100644 index 0000000000000..ccaf85b504282 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/uploads.ts @@ -0,0 +1,114 @@ +import type { IAppServerOrchestrator, IAppsUpload, IAppUploadsConverter } from '@rocket.chat/apps'; +import type { IUploadDetails } from '@rocket.chat/apps-engine/definition/uploads/IUploadDetails'; +import type { IUpload } from '@rocket.chat/core-typings'; +import { Uploads } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; + +export class AppUploadsConverter implements IAppUploadsConverter { + constructor(public orch: IAppServerOrchestrator) {} + + async convertById(uploadId: string): Promise { + const upload = await Uploads.findOneById(uploadId); + + return this.convertToApp(upload); + } + + convertToApp(upload: undefined | null): Promise; + + convertToApp(upload: IUpload): Promise; + + convertToApp(upload: IUpload | undefined | null): Promise; + + async convertToApp(upload: IUpload | undefined | null): Promise { + if (!upload) { + return undefined; + } + + const map = { + id: '_id', + name: 'name', + size: 'size', + type: 'type', + store: 'store', + description: 'description', + complete: 'complete', + uploading: 'uploading', + extension: 'extension', + progress: 'progress', + etag: 'etag', + path: 'path', + token: 'token', + url: 'url', + updatedAt: '_updatedAt', + uploadedAt: 'uploadedAt', + room: async (upload: IUpload) => { + const result = await this.orch.getConverters().get('rooms').convertById(upload.rid!); + delete upload.rid; + return result!; + }, + user: async (upload: IUpload) => { + if (!upload.userId) { + return undefined; + } + + const result = await this.orch.getConverters().get('users').convertById(upload.userId); + delete upload.userId; + return result; + }, + visitor: async (upload: IUpload) => { + if (!(upload as IUploadDetails).visitorToken) { + return undefined; + } + + const result = await this.orch + .getConverters() + .get('visitors') + .convertByToken((upload as IUploadDetails).visitorToken!); + delete (upload as IUploadDetails).visitorToken; + return result; + }, + }; + + return transformMappedData(upload, map); + } + + convertToRocketChat(upload: undefined | null): undefined; + + convertToRocketChat(upload: IAppsUpload): IUpload; + + convertToRocketChat(upload: IAppsUpload | undefined | null): IUpload | undefined; + + convertToRocketChat(upload: IAppsUpload | undefined | null): IUpload | undefined { + if (!upload) { + return undefined; + } + + const { id: userId } = upload.user || {}; + const { token: visitorToken } = upload.visitor || {}; + const { id: rid } = upload.room; + + const newUpload: IUpload = { + _id: upload.id, + name: upload.name, + size: Number(upload.size), + type: upload.type, + extension: upload.extension, + description: (upload as any).description, + store: upload.store, + etag: upload.etag, + complete: upload.complete, + uploading: upload.uploading, + progress: upload.progress, + token: upload.token, + url: upload.url, + ...{ _updatedAt: upload.updatedAt }, // FIXME + uploadedAt: upload.uploadedAt, + rid, + userId, + ...{ visitorToken }, // FIXME + }; + + return Object.assign(newUpload, (upload as { _unmappedProperties_?: unknown })._unmappedProperties_); + } +} From 29960c27caee08195e24dbf2f7c7d2b1309db09b Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 15:06:19 -0300 Subject: [PATCH 11/80] refactor: convert `apps/meteor/app/apps/server/converters/users.js` to TypeScript --- .../server/converters/{users.js => users.ts} | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) rename apps/meteor/app/apps/server/converters/{users.js => users.ts} (56%) diff --git a/apps/meteor/app/apps/server/converters/users.js b/apps/meteor/app/apps/server/converters/users.ts similarity index 56% rename from apps/meteor/app/apps/server/converters/users.js rename to apps/meteor/app/apps/server/converters/users.ts index fc560185fc8d5..bb268759eed22 100644 --- a/apps/meteor/app/apps/server/converters/users.js +++ b/apps/meteor/app/apps/server/converters/users.ts @@ -1,25 +1,31 @@ +import type { IAppServerOrchestrator, IAppsUser, IAppUsersConverter } from '@rocket.chat/apps'; import { UserStatusConnection, UserType } from '@rocket.chat/apps-engine/definition/users'; +import type { IUser, UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { removeEmpty } from '@rocket.chat/tools'; -export class AppUsersConverter { - constructor(orch) { - this.orch = orch; - } +export class AppUsersConverter implements IAppUsersConverter { + constructor(public orch: IAppServerOrchestrator) {} - async convertById(userId) { + async convertById(userId: IUser['_id']): Promise { const user = await Users.findOneById(userId); return this.convertToApp(user); } - async convertByUsername(username) { - const user = await Users.findOneByUsername(username); + async convertByUsername(username: IUser['username']): Promise { + const user = await Users.findOneByUsername(username!); return this.convertToApp(user); } - convertToApp(user) { + convertToApp(user: undefined | null): undefined; + + convertToApp(user: IUser): IAppsUser; + + convertToApp(user: IUser | undefined | null): IAppsUser | undefined; + + convertToApp(user: IUser | undefined | null): IAppsUser | undefined { if (!user) { return undefined; } @@ -29,21 +35,21 @@ export class AppUsersConverter { return { id: user._id, - username: user.username, - emails: user.emails, + username: user.username!, + emails: user.emails! as IAppsUser['emails'], type, isEnabled: user.active, - name: user.name, + name: user.name!, roles: user.roles, bio: user.bio, - status: user.status, + status: user.status!, statusText: user.statusText, - statusConnection, - utcOffset: user.utcOffset, + statusConnection: statusConnection as UserStatusConnection, + utcOffset: user.utcOffset!, createdAt: user.createdAt, updatedAt: user._updatedAt, - lastLoginAt: user.lastLogin, - appId: user.appId, + lastLoginAt: user.lastLogin!, + appId: (user as any).appId, // FIXME customFields: user.customFields, settings: { preferences: { @@ -53,7 +59,13 @@ export class AppUsersConverter { }; } - convertToRocketChat(user) { + convertToRocketChat(user: undefined | null): undefined; + + convertToRocketChat(user: IAppsUser): IUser; + + convertToRocketChat(user: IAppsUser | undefined | null): IUser | undefined; + + convertToRocketChat(user: IAppsUser | undefined | null): IUser | undefined { if (!user) { return undefined; } @@ -67,9 +79,9 @@ export class AppUsersConverter { name: user.name, roles: user.roles, bio: user.bio, - status: user.status, + status: user.status as UserStatus, statusConnection: user.statusConnection, - utcOffset: user.utfOffset, + utcOffset: user.utcOffset, createdAt: user.createdAt, _updatedAt: user.updatedAt, lastLogin: user.lastLoginAt, @@ -77,7 +89,7 @@ export class AppUsersConverter { }); } - _convertUserTypeToEnum(type) { + private _convertUserTypeToEnum(type: string): UserType { switch (type) { case 'user': return UserType.USER; @@ -90,11 +102,15 @@ export class AppUsersConverter { return UserType.UNKNOWN; default: console.warn(`A new user type has been added that the Apps don't know about? "${type}"`); - return type.toUpperCase(); + return type.toUpperCase() as UserType; } } - _convertStatusConnectionToEnum(username, userId, status) { + private _convertStatusConnectionToEnum( + username: IUser['username'], + userId: IUser['_id'], + status: string | undefined, + ): UserStatusConnection { switch (status) { case 'offline': return UserStatusConnection.OFFLINE; @@ -111,7 +127,7 @@ export class AppUsersConverter { console.warn( `The user ${username} (${userId}) does not have a valid status (offline, online, away, or busy). It is currently: "${status}"`, ); - return !status ? UserStatusConnection.OFFLINE : status.toUpperCase(); + return !status ? UserStatusConnection.OFFLINE : (status.toUpperCase() as UserStatusConnection); } } } From 97eedd23a939aca09b3da90a8eae2bb49356fccc Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 15:12:19 -0300 Subject: [PATCH 12/80] refactor: convert `apps/meteor/app/apps/server/converters/visitors.js` to TypeScript --- .../app/apps/server/converters/visitors.js | 64 ---------------- .../app/apps/server/converters/visitors.ts | 76 +++++++++++++++++++ 2 files changed, 76 insertions(+), 64 deletions(-) delete mode 100644 apps/meteor/app/apps/server/converters/visitors.js create mode 100644 apps/meteor/app/apps/server/converters/visitors.ts diff --git a/apps/meteor/app/apps/server/converters/visitors.js b/apps/meteor/app/apps/server/converters/visitors.js deleted file mode 100644 index 00b8b3888ae74..0000000000000 --- a/apps/meteor/app/apps/server/converters/visitors.js +++ /dev/null @@ -1,64 +0,0 @@ -import { LivechatVisitors } from '@rocket.chat/models'; - -import { transformMappedData } from './transformMappedData'; - -// TODO: check if functions from this converter can be async -export class AppVisitorsConverter { - constructor(orch) { - this.orch = orch; - } - - async convertById(id) { - const visitor = await LivechatVisitors.findOneEnabledById(id); - - return this.convertVisitor(visitor); - } - - async convertByToken(token) { - const visitor = await LivechatVisitors.getVisitorByToken(token); - - return this.convertVisitor(visitor); - } - - async convertVisitor(visitor) { - if (!visitor) { - return undefined; - } - - const map = { - id: '_id', - username: 'username', - name: 'name', - department: 'department', - updatedAt: '_updatedAt', - token: 'token', - phone: 'phone', - visitorEmails: 'visitorEmails', - livechatData: 'livechatData', - status: 'status', - activity: 'activity', - }; - - return transformMappedData(visitor, map); - } - - convertAppVisitor(visitor) { - if (!visitor) { - return undefined; - } - - const newVisitor = { - _id: visitor.id, - username: visitor.username, - name: visitor.name, - token: visitor.token, - phone: visitor.phone, - livechatData: visitor.livechatData, - status: visitor.status || 'online', - ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), - ...(visitor.department && { department: visitor.department }), - }; - - return Object.assign(newVisitor, visitor._unmappedProperties_); - } -} diff --git a/apps/meteor/app/apps/server/converters/visitors.ts b/apps/meteor/app/apps/server/converters/visitors.ts new file mode 100644 index 0000000000000..91722dfdd3f7e --- /dev/null +++ b/apps/meteor/app/apps/server/converters/visitors.ts @@ -0,0 +1,76 @@ +import type { IAppServerOrchestrator, IAppsVisitor, IAppVisitorsConverter } from '@rocket.chat/apps'; +import { UserStatus, type ILivechatVisitor } from '@rocket.chat/core-typings'; +import { LivechatVisitors } from '@rocket.chat/models'; + +import { transformMappedData } from './transformMappedData'; + +// TODO: check if functions from this converter can be async +export class AppVisitorsConverter implements IAppVisitorsConverter { + constructor(public orch: IAppServerOrchestrator) {} + + async convertById(visitorId: ILivechatVisitor['_id']): Promise { + const visitor = await LivechatVisitors.findOneEnabledById(visitorId); + + return this.convertVisitor(visitor); + } + + async convertByToken(token: string): Promise { + const visitor = await LivechatVisitors.getVisitorByToken(token); + + return this.convertVisitor(visitor); + } + + convertVisitor(visitor: undefined | null): Promise; + + convertVisitor(visitor: ILivechatVisitor): Promise; + + convertVisitor(visitor: ILivechatVisitor | undefined | null): Promise; + + async convertVisitor(visitor: ILivechatVisitor | undefined | null): Promise { + if (!visitor) { + return undefined; + } + + const map = { + id: '_id', + username: 'username', + name: 'name', + department: 'department', + updatedAt: '_updatedAt', + token: 'token', + phone: 'phone', + visitorEmails: 'visitorEmails', + livechatData: 'livechatData', + status: 'status', + activity: 'activity', + }; + + return transformMappedData(visitor, map); + } + + convertAppVisitor(visitor: undefined | null): undefined; + + convertAppVisitor(visitor: IAppsVisitor): ILivechatVisitor; + + convertAppVisitor(visitor: IAppsVisitor | undefined | null): ILivechatVisitor | undefined; + + convertAppVisitor(visitor: IAppsVisitor | undefined | null): ILivechatVisitor | undefined { + if (!visitor) { + return undefined; + } + + const newVisitor: Partial = { + _id: visitor.id!, + username: visitor.username, + name: visitor.name, + token: visitor.token, + phone: visitor.phone, + livechatData: visitor.livechatData, + status: (visitor.status as UserStatus | undefined) || UserStatus.ONLINE, + ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), + ...(visitor.department && { department: visitor.department }), + }; + + return Object.assign(newVisitor, (visitor as { _unmappedProperties?: any })._unmappedProperties); + } +} From da6c3b9dd30f83621f2061293337b1812ab8c9c5 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 15:31:55 -0300 Subject: [PATCH 13/80] refactor: convert `apps/meteor/app/custom-oauth/server/transform_helpers.js` to TypeScript --- ...nsform_helpers.js => transform_helpers.ts} | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) rename apps/meteor/app/custom-oauth/server/{transform_helpers.js => transform_helpers.ts} (84%) diff --git a/apps/meteor/app/custom-oauth/server/transform_helpers.js b/apps/meteor/app/custom-oauth/server/transform_helpers.ts similarity index 84% rename from apps/meteor/app/custom-oauth/server/transform_helpers.js rename to apps/meteor/app/custom-oauth/server/transform_helpers.ts index ded142bf1e241..1238538a8b74d 100644 --- a/apps/meteor/app/custom-oauth/server/transform_helpers.js +++ b/apps/meteor/app/custom-oauth/server/transform_helpers.ts @@ -1,6 +1,8 @@ -import { isObject } from '../../../lib/utils/isObject'; +import { isObject } from '@rocket.chat/tools'; -export const normalizers = { +type Identity = Record; + +export const normalizers: Record Identity | void> = { // Set 'id' to '_id' for any sources that provide it _id(identity) { if (identity._id && !identity.id) { @@ -37,8 +39,8 @@ export const normalizers = { // Fix Dataporten having 'user.userid' instead of 'id' dataporten(identity) { - if (identity.user && identity.user.userid && !identity.id) { - if (identity.user.userid_sec && identity.user.userid_sec[0]) { + if (identity.user?.userid && !identity.id) { + if (identity.user.userid_sec?.[0]) { identity.id = identity.user.userid_sec[0]; } else { identity.id = identity.user.userid; @@ -49,7 +51,7 @@ export const normalizers = { // Fix for Xenforo [BD]API plugin for 'user.user_id; instead of 'id' xenforo(identity) { - if (identity.user && identity.user.user_id && !identity.id) { + if (identity.user?.user_id && !identity.id) { identity.id = identity.user.user_id; identity.email = identity.user.user_email; } @@ -102,7 +104,7 @@ export const normalizers = { }; const IDENTITY_PROPNAME_FILTER = /(\.)/g; -export const renameInvalidProperties = (input) => { +export const renameInvalidProperties = (input: any): any => { if (Array.isArray(input)) { return input.map(renameInvalidProperties); } @@ -119,12 +121,12 @@ export const renameInvalidProperties = (input) => { ); }; -export const getNestedValue = (propertyPath, source) => +export const getNestedValue = (propertyPath: string, source: any): any => propertyPath.split('.').reduce((prev, curr) => (prev ? prev[curr] : undefined), source); // /^(.+)@/::email const REGEXP_FROM_FORMULA = /^\/((?!\/::).*)\/::(.+)/; -export const getRegexpMatch = (formula, data) => { +export const getRegexpMatch = (formula: string, data: any): any => { const regexAndPath = REGEXP_FROM_FORMULA.exec(formula); if (!regexAndPath) { return getNestedValue(formula, data); @@ -151,10 +153,10 @@ export const getRegexpMatch = (formula, data) => { }; const templateStringRegex = /{{((?:(?!}}).)+)}}/g; -export const fromTemplate = (template, data) => { +export const fromTemplate = (template: string, data: any): any => { if (!templateStringRegex.test(template)) { return getNestedValue(template, data); } - return template.replace(templateStringRegex, (fullMatch, match) => getRegexpMatch(match, data)); + return template.replace(templateStringRegex, (_, match) => getRegexpMatch(match, data)); }; From 4d48f75b76c4127ac2f2db1133e2789a4e9d9763 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 15:56:54 -0300 Subject: [PATCH 14/80] refactor: convert `apps/meteor/app/custom-oauth/server/custom_oauth_server.js` to TypeScript --- .../server/custom_oauth_server.d.ts | 7 - ...oauth_server.js => custom_oauth_server.ts} | 220 +++++++++++++----- 2 files changed, 160 insertions(+), 67 deletions(-) delete mode 100644 apps/meteor/app/custom-oauth/server/custom_oauth_server.d.ts rename apps/meteor/app/custom-oauth/server/{custom_oauth_server.js => custom_oauth_server.ts} (71%) diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.d.ts b/apps/meteor/app/custom-oauth/server/custom_oauth_server.d.ts deleted file mode 100644 index 670b803ebef13..0000000000000 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class CustomOAuth { - constructor(name: string, options: Record); - - getIdentity(accessToken: string, query: Record): any; - - configure(options: Record): any; -} diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.ts similarity index 71% rename from apps/meteor/app/custom-oauth/server/custom_oauth_server.js rename to apps/meteor/app/custom-oauth/server/custom_oauth_server.ts index b9071709f2cce..6bfac007f7664 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.ts @@ -7,6 +7,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; import { ServiceConfiguration } from 'meteor/service-configuration'; +import type { ClientSession } from 'mongodb'; import _ from 'underscore'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; @@ -20,11 +21,108 @@ import { settings } from '../../settings/server'; const logger = new Logger('CustomOAuth'); -const Services = {}; -const BeforeUpdateOrCreateUserFromExternalService = []; +type Identity = Record; + +type ServiceData = { + _OAuthCustom: boolean; + serverURL: string; + accessToken: string; + idToken?: string; + expiresAt: number; + refreshToken?: string; + id?: string; + username?: string; + email?: string; + name?: string; + avatarUrl?: string; + [key: string]: any; +}; + +type OAuthQuery = { + code: string; + state: string; +}; + +type AccessTokenResponse = { + access_token: string; + id_token?: string; + expires_in: string; + refresh_token?: string; + error?: string; +}; + +type CustomOAuthOptions = { + serverURL?: string; + tokenPath?: string; + identityPath?: string; + authorizePath?: string; + scope?: string; + loginStyle?: string; + accessTokenParam?: string; + tokenSentVia?: string; + identityTokenSentVia?: string | null; + keyField?: string; + usernameField?: string; + emailField?: string; + nameField?: string; + avatarField?: string; + mergeUsers?: boolean; + mergeUsersDistinctServices?: boolean; + rolesClaim?: string; + groupsClaim?: string; + mapChannels?: boolean; + channelsMap?: string; + channelsAdmin?: string; + mergeRoles?: boolean; + rolesToSync?: string; + showButton?: boolean; + addAutopublishFields?: Record; +}; + +type ExtendedSession = ClientSession & { + onceSuccesfulCommit: (cb: () => void | Promise) => void; +}; + +const Services: Record = {}; +const BeforeUpdateOrCreateUserFromExternalService: Array<(serviceName: string, serviceData: ServiceData, options?: any) => Promise> = + []; export class CustomOAuth { - constructor(name, options) { + name: string; + + serverURL: string; + + tokenPath: string; + + identityPath: string; + + tokenSentVia?: string; + + identityTokenSentVia?: string | null; + + keyField?: string; + + usernameField: string; + + emailField: string; + + nameField: string; + + avatarField: string; + + mergeUsers?: boolean; + + mergeUsersDistinctServices?: boolean; + + rolesClaim: string; + + accessTokenParam: string; + + channelsAdmin: string; + + userAgent: string; + + constructor(name: string, options: CustomOAuthOptions) { logger.debug({ msg: 'Init CustomOAuth', name, options }); this.name = name; @@ -49,10 +147,10 @@ export class CustomOAuth { Accounts.oauth.registerService(this.name); this.registerService(); this.addHookToProcessUser(); - this.registerAccessTokenService(this.name, this.accessTokenParam); + this.registerAccessTokenService(this.name); } - configure(options) { + configure(options: CustomOAuthOptions): void { if (!Match.test(options, Object)) { throw new Meteor.Error('CustomOAuth: Options is required and must be Object'); } @@ -100,28 +198,24 @@ export class CustomOAuth { if (!isURL(this.identityPath)) { this.identityPath = this.serverURL + this.identityPath; } - - if (Match.test(options.addAutopublishFields, Object)) { - Accounts.addAutopublishFields(options.addAutopublishFields); - } } - async getAccessToken(query) { + async getAccessToken(query: OAuthQuery): Promise { const config = await ServiceConfiguration.configurations.findOneAsync({ service: this.name }); if (!config) { throw new Accounts.ConfigError(); } - let response = undefined; + let response: AccessTokenResponse | undefined = undefined; - const headers = { + const headers: Record = { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user 'Accept': 'application/json', }; const params = new URLSearchParams({ code: query.code, - redirect_uri: OAuth._redirectUri(this.name, config), + redirect_uri: OAuth._redirectUri(this.name, config as any), grant_type: 'authorization_code', state: query.state, }); @@ -132,7 +226,7 @@ export class CustomOAuth { headers.Authorization = `Basic ${b64}`; } else { params.append('client_secret', config.secret); - params.append('client_id', config.clientId); + params.append('client_id', config.clientId || ''); } try { @@ -147,11 +241,15 @@ export class CustomOAuth { } response = await request.json(); - } catch (err) { + } catch (err: any) { const error = new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${err.message}`); throw _.extend(error, { response: err.response }); } + if (!response) { + throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. No response received`); + } + if (response.error) { // if the http response was a json object with an error attribute throw new Error(`Failed to complete OAuth handshake with ${this.name} at ${this.tokenPath}. ${response.error}`); @@ -160,9 +258,9 @@ export class CustomOAuth { } } - async getIdentity(accessToken) { - const params = {}; - const headers = { + async getIdentity(accessToken: string, _query?: any): Promise { + const params: Record = {}; + const headers: Record = { 'User-Agent': this.userAgent, // http://doc.gitlab.com/ce/api/users.html#Current-user 'Accept': 'application/json', }; @@ -185,21 +283,20 @@ export class CustomOAuth { logger.debug({ msg: 'Identity response', response }); return this.normalizeIdentity(response); - } catch (err) { + } catch (err: any) { const error = new Error(`Failed to fetch identity from ${this.name} at ${this.identityPath}. ${err.message}`); throw _.extend(error, { response: err.response }); } } - registerService() { - const self = this; - OAuth.registerService(this.name, 2, null, async (query) => { - const response = await self.getAccessToken(query); - const identity = await self.getIdentity(response.access_token, query); + registerService(): void { + (OAuth as any).registerService(this.name, 2, null, async (query: OAuthQuery) => { + const response = await this.getAccessToken(query); + const identity = await this.getIdentity(response.access_token); - const serviceData = { + const serviceData: ServiceData = { _OAuthCustom: true, - serverURL: self.serverURL, + serverURL: this.serverURL, accessToken: response.access_token, idToken: response.id_token, expiresAt: +new Date() + 1000 * parseInt(response.expires_in, 10), @@ -227,7 +324,7 @@ export class CustomOAuth { }); } - normalizeIdentity(identity) { + normalizeIdentity(identity: Identity): Identity { if (identity) { for (const normalizer of Object.values(normalizers)) { const result = normalizer(identity); @@ -258,11 +355,11 @@ export class CustomOAuth { return renameInvalidProperties(identity); } - retrieveCredential(credentialToken, credentialSecret) { + retrieveCredential(credentialToken: string, credentialSecret: string): any { return OAuth.retrieveCredential(credentialToken, credentialSecret); } - getUsername(data) { + getUsername(data: Identity): string { try { const value = fromTemplate(this.usernameField, data); @@ -270,12 +367,12 @@ export class CustomOAuth { throw new Meteor.Error('field_not_found', `Username field "${this.usernameField}" not found in data`, data); } return value; - } catch (error) { + } catch (error: any) { throw new Error('CustomOAuth: Failed to extract username', error.message); } } - getEmail(data) { + getEmail(data: Identity): string { try { const value = fromTemplate(this.emailField, data); @@ -283,12 +380,12 @@ export class CustomOAuth { throw new Meteor.Error('field_not_found', `Email field "${this.emailField}" not found in data`, data); } return value; - } catch (error) { + } catch (error: any) { throw new Error('CustomOAuth: Failed to extract email', error.message); } } - getCustomName(data) { + getCustomName(data: Identity): string { try { const value = fromTemplate(this.nameField, data); @@ -297,12 +394,12 @@ export class CustomOAuth { } return value; - } catch (error) { + } catch (error: any) { throw new Error('CustomOAuth: Failed to extract custom name', error.message); } } - getAvatarUrl(data) { + getAvatarUrl(data: Identity): string | undefined { try { const value = fromTemplate(this.avatarField, data); @@ -310,12 +407,12 @@ export class CustomOAuth { logger.debug({ msg: 'Avatar field not found in data', avatarField: this.avatarField, data }); } return value; - } catch (error) { + } catch (error: any) { throw new Error('CustomOAuth: Failed to extract avatar url', error.message); } } - getName(identity) { + getName(identity: Identity): string { const name = identity.name || identity.username || @@ -327,23 +424,23 @@ export class CustomOAuth { return name; } - addHookToProcessUser() { + addHookToProcessUser(): void { BeforeUpdateOrCreateUserFromExternalService.push(async (serviceName, serviceData /* , options*/) => { if (serviceName !== this.name) { return; } if (serviceData.username) { - let user = undefined; + let user: any = undefined; if (this.keyField === 'username') { user = this.mergeUsersDistinctServices ? await Users.findOneByUsernameIgnoringCase(serviceData.username) - : await Users.findOneByUsernameAndServiceNameIgnoringCase(serviceData.username, serviceData.id, serviceName); + : await Users.findOneByUsernameAndServiceNameIgnoringCase(serviceData.username, serviceData.id || '', serviceName); } else if (this.keyField === 'email') { user = this.mergeUsersDistinctServices - ? await Users.findOneByEmailAddress(serviceData.email) - : await Users.findOneByEmailAddressAndServiceNameIgnoringCase(serviceData.email, serviceData.id, serviceName); + ? await Users.findOneByEmailAddress(serviceData.email || '') + : await Users.findOneByEmailAddressAndServiceNameIgnoringCase(serviceData.email || '', serviceData.id || '', serviceName); } if (!user) { @@ -358,7 +455,7 @@ export class CustomOAuth { user.services[serviceName] && user.services[serviceName].id === serviceData.id && user.name === serviceData.name && - (this.keyField === 'email' || !serviceData.email || user.emails?.find(({ address }) => address === serviceData.email)) + (this.keyField === 'email' || !serviceData.email || user.emails?.find(({ address }: any) => address === serviceData.email)) ) { return; } @@ -368,7 +465,7 @@ export class CustomOAuth { } const serviceIdKey = `services.${serviceName}.id`; - const successCallbacks = [ + const successCallbacks: Array<() => void | Promise> = [ async () => { const updatedUser = await Users.findOneById(user._id, { projection: { name: 1, emails: 1, [serviceIdKey]: 1 } }); if (updatedUser) { @@ -382,7 +479,7 @@ export class CustomOAuth { try { // Extend the session to match the ExtendedSession type expected by saveUserIdentity Object.assign(session, { - onceSuccesfulCommit: (cb) => { + onceSuccesfulCommit: (cb: () => void | Promise) => { successCallbacks.push(cb); }, }); @@ -395,13 +492,13 @@ export class CustomOAuth { updater.set('emails', [{ address: serviceData.email, verified: true }]); } - updater.set(serviceIdKey, serviceData.id); + (updater as any).set(serviceIdKey, serviceData.id); await saveUserIdentity({ _id: user._id, name: serviceData.name, updater, - session, + session: session as ExtendedSession, updateUsernameInBackground: true, // Username needs to be included otherwise the name won't be updated in some collections username: user.username, @@ -420,7 +517,7 @@ export class CustomOAuth { } }); - Accounts.validateNewUser((user) => { + Accounts.validateNewUser((user: any) => { if (!user.services || !user.services[this.name] || !user.services[this.name].id) { return true; } @@ -441,11 +538,10 @@ export class CustomOAuth { }); } - registerAccessTokenService(name) { - const self = this; + registerAccessTokenService(name: string): void { const whitelisted = ['id', 'email', 'username', 'name', this.rolesClaim]; - registerAccessTokenService(name, async (options) => { + registerAccessTokenService(name, async (options: { accessToken: string; expiresIn: number }) => { check( options, Match.ObjectIncluding({ @@ -454,11 +550,13 @@ export class CustomOAuth { }), ); - const identity = await self.getIdentity(options.accessToken); + const identity = await this.getIdentity(options.accessToken); - const serviceData = { + const serviceData: ServiceData = { + _OAuthCustom: true, + serverURL: this.serverURL, accessToken: options.accessToken, - expiresAt: +new Date() + 1000 * parseInt(options.expiresIn, 10), + expiresAt: +new Date() + 1000 * parseInt(options.expiresIn.toString(), 10), }; const fields = _.pick(identity, whitelisted); @@ -478,20 +576,22 @@ export class CustomOAuth { const { updateOrCreateUserFromExternalService } = Accounts; -Accounts.updateOrCreateUserFromExternalService = async function (...args /* serviceName, serviceData, options*/) { +(Accounts as any).updateOrCreateUserFromExternalService = async function (serviceName: string, serviceData: ServiceData, options?: any) { for await (const hook of BeforeUpdateOrCreateUserFromExternalService) { - await hook.apply(this, args); + await hook.call(this, serviceName, serviceData, options); } - const [serviceName, serviceData] = args; - - const user = await updateOrCreateUserFromExternalService.apply(this, args); + const user = await (updateOrCreateUserFromExternalService as any).call(this, serviceName, serviceData, options); if (!user.userId) { return undefined; } const fullUser = await Users.findOneById(user.userId); - if (settings.get('LDAP_Update_Data_On_OAuth_Login')) { + if (!fullUser) { + return user; + } + + if (settings.get('LDAP_Update_Data_On_OAuth_Login') && fullUser.username) { await LDAP.loginAuthenticatedUserRequest(fullUser.username); } From 587729ad571cf3e8415c3ae6a539d6532e9c7fd1 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 16:13:07 -0300 Subject: [PATCH 15/80] refactor: convert `apps/meteor/app/custom-sounds/server/startup/custom-sounds.js` to TypeScript --- .../{custom-sounds.js => custom-sounds.ts} | 27 ++++++++++--------- apps/meteor/app/file/server/file.server.ts | 4 +-- apps/meteor/app/file/server/index.ts | 4 +-- 3 files changed, 18 insertions(+), 17 deletions(-) rename apps/meteor/app/custom-sounds/server/startup/{custom-sounds.js => custom-sounds.ts} (64%) diff --git a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.ts similarity index 64% rename from apps/meteor/app/custom-sounds/server/startup/custom-sounds.js rename to apps/meteor/app/custom-sounds/server/startup/custom-sounds.ts index 117a7d3c9e759..beaa8b356b947 100644 --- a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js +++ b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.ts @@ -1,3 +1,5 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; @@ -5,13 +7,13 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { RocketChatFile } from '../../../file/server'; import { settings } from '../../../settings/server'; -export let RocketChatFileCustomSoundsInstance; +export let RocketChatFileCustomSoundsInstance: InstanceType; Meteor.startup(() => { - let storeType = 'GridFS'; + let storeType: 'GridFS' | 'FileSystem' = 'GridFS'; - if (settings.get('CustomSounds_Storage_Type')) { - storeType = settings.get('CustomSounds_Storage_Type'); + if (settings.get<'GridFS' | 'FileSystem'>('CustomSounds_Storage_Type')) { + storeType = settings.get<'GridFS' | 'FileSystem'>('CustomSounds_Storage_Type'); } const RocketChatStore = RocketChatFile[storeType]; @@ -26,9 +28,10 @@ Meteor.startup(() => { }); let path = '~/uploads'; - if (settings.get('CustomSounds_FileSystemPath') != null) { - if (settings.get('CustomSounds_FileSystemPath').trim() !== '') { - path = settings.get('CustomSounds_FileSystemPath'); + if (settings.get('CustomSounds_FileSystemPath') != null) { + const filePath = settings.get('CustomSounds_FileSystemPath'); + if (typeof filePath === 'string' && filePath.trim() !== '') { + path = filePath; } } @@ -37,8 +40,8 @@ Meteor.startup(() => { absolutePath: path, }); - return WebApp.connectHandlers.use('/custom-sounds/', async (req, res /* , next*/) => { - const fileId = decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')); + return WebApp.connectHandlers.use('/custom-sounds/', async (req: IncomingMessage, res: ServerResponse /* , next*/) => { + const fileId = decodeURIComponent(req.url?.replace(/^\//, '').replace(/\?.*$/, '') || ''); if (!fileId) { res.writeHead(403); @@ -57,8 +60,8 @@ Meteor.startup(() => { res.setHeader('Content-Disposition', 'inline'); - let fileUploadDate = undefined; - if (file.uploadDate != null) { + let fileUploadDate: string | undefined = undefined; + if ('uploadDate' in file && file.uploadDate != null) { fileUploadDate = file.uploadDate.toUTCString(); } @@ -80,7 +83,7 @@ Meteor.startup(() => { res.setHeader('Last-Modified', new Date().toUTCString()); } - res.setHeader('Content-Type', file.contentType); + res.setHeader('Content-Type', file.contentType!); res.setHeader('Content-Length', file.length); file.readStream.pipe(res); diff --git a/apps/meteor/app/file/server/file.server.ts b/apps/meteor/app/file/server/file.server.ts index 0af01c1bfdab7..30d96f0e97563 100644 --- a/apps/meteor/app/file/server/file.server.ts +++ b/apps/meteor/app/file/server/file.server.ts @@ -22,9 +22,9 @@ type IFile = { interface IRocketChatFileStore { remove(fileId: string): Promise; - createWriteStream(fileName: string, contentType: string): void; + createWriteStream(fileName: string, contentType: string): unknown; - createReadStream(fileName: string): void; + createReadStream(fileName: string): unknown; getFileWithReadStream(fileName: string): Promise< | { diff --git a/apps/meteor/app/file/server/index.ts b/apps/meteor/app/file/server/index.ts index dee3df2abb29b..f2288c8ec47b2 100644 --- a/apps/meteor/app/file/server/index.ts +++ b/apps/meteor/app/file/server/index.ts @@ -1,3 +1 @@ -import { RocketChatFile } from './file.server'; - -export { RocketChatFile }; +export { RocketChatFile } from './file.server'; From d3cbbf0c352a456d8d65685faf8afc81957010e3 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 16:51:48 -0300 Subject: [PATCH 16/80] refactor: convert `apps/meteor/app/emoji-custom/server/startup/emoji-custom.js` to TypeScript --- .../server/lib/insertOrUpdateEmoji.ts | 2 +- .../{emoji-custom.js => emoji-custom.ts} | 27 ++++++++++--------- 2 files changed, 16 insertions(+), 13 deletions(-) rename apps/meteor/app/emoji-custom/server/startup/{emoji-custom.js => emoji-custom.ts} (71%) diff --git a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts index 85a9648cf6d98..6454e5ac525f5 100644 --- a/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts +++ b/apps/meteor/app/emoji-custom/server/lib/insertOrUpdateEmoji.ts @@ -126,7 +126,7 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`)); const ws = RocketChatFileEmojiCustomInstance.createWriteStream( encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`), - rs.contentType, + rs.contentType!, ); ws.on('end', () => RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`)), diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.ts similarity index 71% rename from apps/meteor/app/emoji-custom/server/startup/emoji-custom.js rename to apps/meteor/app/emoji-custom/server/startup/emoji-custom.ts index fed5123b115f4..2475f3e798155 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.ts @@ -1,3 +1,5 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import _ from 'underscore'; @@ -6,13 +8,13 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { RocketChatFile } from '../../../file/server'; import { settings } from '../../../settings/server'; -export let RocketChatFileEmojiCustomInstance; +export let RocketChatFileEmojiCustomInstance: InstanceType; Meteor.startup(() => { - let storeType = 'GridFS'; + let storeType: 'GridFS' | 'FileSystem' = 'GridFS'; - if (settings.get('EmojiUpload_Storage_Type')) { - storeType = settings.get('EmojiUpload_Storage_Type'); + if (settings.get<'GridFS' | 'FileSystem'>('EmojiUpload_Storage_Type')) { + storeType = settings.get<'GridFS' | 'FileSystem'>('EmojiUpload_Storage_Type'); } const RocketChatStore = RocketChatFile[storeType]; @@ -27,9 +29,10 @@ Meteor.startup(() => { }); let path = '~/uploads'; - if (settings.get('EmojiUpload_FileSystemPath') != null) { - if (settings.get('EmojiUpload_FileSystemPath').trim() !== '') { - path = settings.get('EmojiUpload_FileSystemPath'); + if (settings.get('EmojiUpload_FileSystemPath') != null) { + const filePath = settings.get('EmojiUpload_FileSystemPath'); + if (typeof filePath === 'string' && filePath.trim() !== '') { + path = filePath; } } @@ -38,8 +41,8 @@ Meteor.startup(() => { absolutePath: path, }); - return WebApp.connectHandlers.use('/emoji-custom/', async (req, res /* , next*/) => { - const params = { emoji: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) }; + return WebApp.connectHandlers.use('/emoji-custom/', async (req: IncomingMessage, res: ServerResponse /* , next*/) => { + const params = { emoji: decodeURIComponent(req.url?.replace(/^\//, '').replace(/\?.*$/, '') || '') }; if (_.isEmpty(params.emoji)) { res.writeHead(403); @@ -83,7 +86,7 @@ Meteor.startup(() => { return; } - const fileUploadDate = file.uploadDate != null ? file.uploadDate.toUTCString() : undefined; + const fileUploadDate = 'uploadDate' in file && file.uploadDate != null ? file.uploadDate.toUTCString() : undefined; const reqModifiedHeader = req.headers['if-modified-since']; if (reqModifiedHeader != null && reqModifiedHeader === fileUploadDate) { @@ -97,9 +100,9 @@ Meteor.startup(() => { res.setHeader('Last-Modified', fileUploadDate || new Date().toUTCString()); res.setHeader('Content-Length', file.length); - if (/^svg$/i.test(params.emoji.split('.').pop())) { + if (/^svg$/i.test(params.emoji.split('.').pop() || '')) { res.setHeader('Content-Type', 'image/svg+xml'); - } else if (/^png$/i.test(params.emoji.split('.').pop())) { + } else if (/^png$/i.test(params.emoji.split('.').pop() || '')) { res.setHeader('Content-Type', 'image/png'); } else { res.setHeader('Content-Type', 'image/jpeg'); From e18747a32dcac6807d61fa4ca41046dbdb668e20 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 17:04:04 -0300 Subject: [PATCH 17/80] refactor: convert `apps/meteor/app/google-oauth/server/index.js` to TypeScript --- .../server/{index.js => index.ts} | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) rename apps/meteor/app/google-oauth/server/{index.js => index.ts} (62%) diff --git a/apps/meteor/app/google-oauth/server/index.js b/apps/meteor/app/google-oauth/server/index.ts similarity index 62% rename from apps/meteor/app/google-oauth/server/index.js rename to apps/meteor/app/google-oauth/server/index.ts index 540d626dafd69..6acc08fb77e18 100644 --- a/apps/meteor/app/google-oauth/server/index.js +++ b/apps/meteor/app/google-oauth/server/index.ts @@ -1,14 +1,35 @@ +import type { ServerResponse } from 'http'; + import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; // The code on this file was copied directly from Meteor and modified to support mobile google oauth // https://github.com/meteor/meteor/blob/ffcfa5062cf1bf8a64ea64fef681ffcd99fe7939/packages/oauth/oauth_server.js +type RenderOptions = { + loginStyle: string; + setCredentialToken: boolean; + credentialToken?: string; + credentialSecret?: string; + redirectUrl?: string; + isCordova: boolean; +}; + +type EndOfLoginDetails = { + loginStyle: string; + query: any; + error?: any; + credentials?: { + token: string; + secret: string; + }; +}; + Meteor.startup(() => { const appRedirectUrl = 'rocketchat://auth'; - const renderEndOfLoginResponse = async (options) => { - const escape = (s) => { + const renderEndOfLoginResponse = async (options: RenderOptions): Promise => { + const escape = (s: string | undefined): string | undefined => { if (!s) { return s; } @@ -26,36 +47,36 @@ Meteor.startup(() => { setCredentialToken: !!options.setCredentialToken, credentialToken: escape(options.credentialToken), credentialSecret: escape(options.credentialSecret), - storagePrefix: escape(OAuth._storageTokenPrefix), + storagePrefix: escape((OAuth as any)._storageTokenPrefix), redirectUrl: escape(options.redirectUrl), isCordova: Boolean(options.isCordova), }; - let template; + let template: string; if (options.loginStyle === 'popup') { - template = await OAuth._endOfPopupResponseTemplate(); + template = await (OAuth as any)._endOfPopupResponseTemplate(); } else if (options.loginStyle === 'redirect') { - template = await OAuth._endOfRedirectResponseTemplate(); + template = await (OAuth as any)._endOfRedirectResponseTemplate(); } else { throw new Error(`invalid loginStyle: ${options.loginStyle}`); } const result = template .replace(/##CONFIG##/, JSON.stringify(config)) - .replace(/##ROOT_URL_PATH_PREFIX##/, __meteor_runtime_config__.ROOT_URL_PATH_PREFIX); + .replace(/##ROOT_URL_PATH_PREFIX##/, (globalThis as any).__meteor_runtime_config__.ROOT_URL_PATH_PREFIX); return `\n${result}`; }; - OAuth._endOfLoginResponse = async (res, details) => { + (OAuth as any)._endOfLoginResponse = async (res: ServerResponse, details: EndOfLoginDetails) => { res.writeHead(200, { 'Content-Type': 'text/html' }); - let redirectUrl; + let redirectUrl: string | undefined; if (details.loginStyle === 'redirect') { - redirectUrl = OAuth._stateFromQuery(details.query).redirectUrl; + redirectUrl = (OAuth as any)._stateFromQuery(details.query).redirectUrl; const appHost = Meteor.absoluteUrl(); - if (redirectUrl.startsWith(appRedirectUrl)) { + if (redirectUrl && redirectUrl.startsWith(appRedirectUrl)) { redirectUrl = `${appRedirectUrl}?host=${appHost}&type=oauth`; if (details.error) { @@ -67,13 +88,16 @@ Meteor.startup(() => { const { token, secret } = details.credentials; redirectUrl = `${redirectUrl}&credentialToken=${token}&credentialSecret=${secret}`; } - } else if (!Meteor.settings?.packages?.oauth?.disableCheckRedirectUrlOrigin && OAuth._checkRedirectUrlOrigin(redirectUrl)) { + } else if ( + !(Meteor.settings as any)?.packages?.oauth?.disableCheckRedirectUrlOrigin && + (OAuth as any)._checkRedirectUrlOrigin(redirectUrl) + ) { details.error = `redirectUrl (${redirectUrl}) is not on the same host as the app (${appHost})`; redirectUrl = appHost; } } - const isCordova = OAuth._isCordovaFromQuery(details.query); + const isCordova = (OAuth as any)._isCordovaFromQuery(details.query); if (details.error) { res.end( @@ -95,8 +119,8 @@ Meteor.startup(() => { await renderEndOfLoginResponse({ loginStyle: details.loginStyle, setCredentialToken: true, - credentialToken: details.credentials.token, - credentialSecret: details.credentials.secret, + credentialToken: details.credentials!.token, + credentialSecret: details.credentials!.secret, redirectUrl, isCordova, }), From 98ca8ebb03051559c65944d4c0aa89f8ae7e57be Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 17:18:07 -0300 Subject: [PATCH 18/80] refactor: convert `apps/meteor/app/importer/server/startup/setImportsToInvalid.js` to TypeScript --- .../startup/{setImportsToInvalid.js => setImportsToInvalid.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename apps/meteor/app/importer/server/startup/{setImportsToInvalid.js => setImportsToInvalid.ts} (94%) diff --git a/apps/meteor/app/importer/server/startup/setImportsToInvalid.js b/apps/meteor/app/importer/server/startup/setImportsToInvalid.ts similarity index 94% rename from apps/meteor/app/importer/server/startup/setImportsToInvalid.js rename to apps/meteor/app/importer/server/startup/setImportsToInvalid.ts index bca3061efea0d..3a12aad1d54b4 100644 --- a/apps/meteor/app/importer/server/startup/setImportsToInvalid.js +++ b/apps/meteor/app/importer/server/startup/setImportsToInvalid.ts @@ -7,7 +7,7 @@ Meteor.startup(async () => { const lastOperation = await Imports.findLastImport(); // If the operation is still on "ready to start" state, we don't need to invalidate it. - if (lastOperation && [ProgressStep.USER_SELECTION].includes(lastOperation.status)) { + if (lastOperation && [ProgressStep.USER_SELECTION].includes(lastOperation.status as any)) { await Imports.invalidateOperationsExceptId(lastOperation._id); return; } From 108c6e73cebc970c01cea1d768a4c422b738c263 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 17:41:02 -0300 Subject: [PATCH 19/80] refactor: convert `apps/meteor/app/importer/server/startup/store.js` to TypeScript --- apps/meteor/app/file/server/file.server.ts | 2 +- .../app/importer/server/methods/uploadImportFile.ts | 2 +- .../importer/server/startup/{store.js => store.ts} | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) rename apps/meteor/app/importer/server/startup/{store.js => store.ts} (53%) diff --git a/apps/meteor/app/file/server/file.server.ts b/apps/meteor/app/file/server/file.server.ts index 30d96f0e97563..36c5c3430f53c 100644 --- a/apps/meteor/app/file/server/file.server.ts +++ b/apps/meteor/app/file/server/file.server.ts @@ -125,7 +125,7 @@ class GridFS implements IRocketChatFileStore { } class FileSystem implements IRocketChatFileStore { - private absolutePath: string; + absolutePath: string; constructor({ absolutePath = '~/uploads' } = {}) { if (absolutePath.split(path.sep)[0] === '~') { diff --git a/apps/meteor/app/importer/server/methods/uploadImportFile.ts b/apps/meteor/app/importer/server/methods/uploadImportFile.ts index df5d2af883fae..12dea98daf0ef 100644 --- a/apps/meteor/app/importer/server/methods/uploadImportFile.ts +++ b/apps/meteor/app/importer/server/methods/uploadImportFile.ts @@ -35,7 +35,7 @@ export const executeUploadImportFile = async ( // Save the file on the File Store const file = Buffer.from(binaryContent, 'base64'); const readStream = RocketChatFile.bufferToStream(file); - const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); + const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName); await new Promise((resolve, reject) => { try { diff --git a/apps/meteor/app/importer/server/startup/store.js b/apps/meteor/app/importer/server/startup/store.ts similarity index 53% rename from apps/meteor/app/importer/server/startup/store.js rename to apps/meteor/app/importer/server/startup/store.ts index 4cdd3baea2ab0..34ecc63eba7d1 100644 --- a/apps/meteor/app/importer/server/startup/store.js +++ b/apps/meteor/app/importer/server/startup/store.ts @@ -3,20 +3,21 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatFile } from '../../../file/server'; import { settings } from '../../../settings/server'; -export let RocketChatImportFileInstance; +export let RocketChatImportFileInstance: InstanceType; Meteor.startup(() => { const RocketChatStore = RocketChatFile.FileSystem; let path = '/tmp/rocketchat-importer'; - if (settings.get('ImportFile_FileSystemPath') != null) { - if (settings.get('ImportFile_FileSystemPath').trim() !== '') { - path = settings.get('ImportFile_FileSystemPath'); + if (settings.get('ImportFile_FileSystemPath') != null) { + const filePath = settings.get('ImportFile_FileSystemPath'); + if (typeof filePath === 'string' && filePath.trim() !== '') { + path = filePath; } } RocketChatImportFileInstance = new RocketChatStore({ name: 'import_files', absolutePath: path, - }); + } as any); // FIXME }); From b876a01b8720ba3f9014d40935e6b3505713468b Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 18:00:59 -0300 Subject: [PATCH 20/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js` to TypeScript --- .../irc-bridge/localHandlers/onCreateRoom.js | 13 ------------- .../irc-bridge/localHandlers/onCreateRoom.ts | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 13 deletions(-) delete mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js create mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.ts diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js deleted file mode 100644 index f0417d311616f..0000000000000 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Users } from '@rocket.chat/models'; - -export default async function handleOnCreateRoom(user, room) { - const users = await Users.findByRoomId(room._id); - - users.forEach((user) => { - if (user.profile?.irc?.fromIRC) { - this.sendCommand('joinChannel', { room, user }); - } else { - this.sendCommand('joinedChannel', { room, user }); - } - }); -} diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.ts b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.ts new file mode 100644 index 0000000000000..43b787fed3002 --- /dev/null +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateRoom.ts @@ -0,0 +1,14 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +export default async function handleOnCreateRoom(this: any, _user: IUser, room: IRoom): Promise { + const users = await Users.findByRoomId(room._id); + + void users.forEach((user) => { + if ((user as any).profile?.irc?.fromIRC) { + this.sendCommand('joinChannel', { room, user }); + } else { + this.sendCommand('joinedChannel', { room, user }); + } + }); +} From ef722750dfaf30c098410dbfbb0515c6a7d48373 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 21:44:41 -0300 Subject: [PATCH 21/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js` to TypeScript --- .../{onCreateUser.js => onCreateUser.ts} | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/localHandlers/{onCreateUser.js => onCreateUser.ts} (68%) diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.ts similarity index 68% rename from apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js rename to apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.ts index 0a9c694c1c7dc..68801a4aa1391 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onCreateUser.ts @@ -1,6 +1,7 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; -export default async function handleOnCreateUser(newUser) { +export default async function handleOnCreateUser(this: any, newUser: IUser): Promise { if (!newUser) { return this.log('Invalid handleOnCreateUser call'); } @@ -29,9 +30,13 @@ export default async function handleOnCreateUser(newUser) { _id: newUser._id, }); + if (!user) { + return; + } + this.sendCommand('registerUser', user); - const rooms = await Rooms.findBySubscriptionUserId(user._id).toArray(); + const rooms = await (await Rooms.findBySubscriptionUserId(user._id)).toArray(); - rooms.forEach((room) => this.sendCommand('joinedChannel', { room, user })); + rooms.forEach((room: any) => this.sendCommand('joinedChannel', { room, user })); } From 72333549818fdf7c2b6c15cd02829aed21f87e4f Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:01:10 -0300 Subject: [PATCH 22/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.js` to TypeScript --- .../app/irc/server/irc-bridge/localHandlers/onJoinRoom.js | 3 --- .../app/irc/server/irc-bridge/localHandlers/onJoinRoom.ts | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.js create mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.ts diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.js deleted file mode 100644 index eb280c1801937..0000000000000 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.js +++ /dev/null @@ -1,3 +0,0 @@ -export default async function handleOnJoinRoom(user, room) { - this.sendCommand('joinedChannel', { room, user }); -} diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.ts b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.ts new file mode 100644 index 0000000000000..195f02bef93a4 --- /dev/null +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onJoinRoom.ts @@ -0,0 +1,5 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; + +export default async function handleOnJoinRoom(this: any, user: IUser, room: IRoom): Promise { + this.sendCommand('joinedChannel', { room, user }); +} From 4457cf1ddc56635e9b592029a3244eb48b2a1099 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:04:27 -0300 Subject: [PATCH 23/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.js` to TypeScript --- .../app/irc/server/irc-bridge/localHandlers/onLeaveRoom.js | 3 --- .../app/irc/server/irc-bridge/localHandlers/onLeaveRoom.ts | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.js create mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.ts diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.js deleted file mode 100644 index e40ce1abaad73..0000000000000 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.js +++ /dev/null @@ -1,3 +0,0 @@ -export default async function handleOnLeaveRoom(user, room) { - this.sendCommand('leftChannel', { room, user }); -} diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.ts b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.ts new file mode 100644 index 0000000000000..94f813e4c6bc7 --- /dev/null +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLeaveRoom.ts @@ -0,0 +1,5 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; + +export default async function handleOnLeaveRoom(this: any, user: IUser, room: IRoom): Promise { + this.sendCommand('leftChannel', { room, user }); +} From 7cbe4f0ca0a9454f91c054f38a76545909da0813 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:07:20 -0300 Subject: [PATCH 24/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js` to TypeScript --- .../localHandlers/{onLogin.js => onLogin.ts} | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/localHandlers/{onLogin.js => onLogin.ts} (72%) diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.ts similarity index 72% rename from apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js rename to apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.ts index 94b5abac5b2d3..9ecbab3a61bfd 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogin.ts @@ -1,6 +1,11 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; -export default async function handleOnLogin(login) { +type Login = { + user: IUser | null; +}; + +export default async function handleOnLogin(this: any, login: Login): Promise { if (login.user === null) { return this.log('Invalid handleOnLogin call'); } @@ -29,10 +34,14 @@ export default async function handleOnLogin(login) { _id: login.user._id, }); + if (!user) { + return; + } + this.sendCommand('registerUser', user); - const rooms = await Rooms.findBySubscriptionUserId(user._id).toArray(); + const rooms = await (await Rooms.findBySubscriptionUserId(user._id)).toArray(); - rooms.forEach((room) => { + rooms.forEach((room: any) => { if (room.t === 'd') { return; } From b2616743ee78a4c99d9502830d5ef32c9390a401 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:10:03 -0300 Subject: [PATCH 25/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.js` to TypeScript --- .../app/irc/server/irc-bridge/localHandlers/onLogout.js | 7 ------- .../app/irc/server/irc-bridge/localHandlers/onLogout.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.js create mode 100644 apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.ts diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.js deleted file mode 100644 index 31153263b7e1a..0000000000000 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.js +++ /dev/null @@ -1,7 +0,0 @@ -import _ from 'underscore'; - -export default async function handleOnLogout(user) { - this.loggedInUsers = _.without(this.loggedInUsers, user._id); - - this.sendCommand('disconnected', { user }); -} diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.ts b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.ts new file mode 100644 index 0000000000000..6cb4b712c00c1 --- /dev/null +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onLogout.ts @@ -0,0 +1,8 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import _ from 'underscore'; + +export default async function handleOnLogout(this: any, user: IUser): Promise { + this.loggedInUsers = _.without(this.loggedInUsers, user._id); + + this.sendCommand('disconnected', { user }); +} From 0edeb9f21e91a69e5d663abb7fdd85ced70c3c34 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:17:03 -0300 Subject: [PATCH 26/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js` to TypeScript --- .../{onSaveMessage.js => onSaveMessage.ts} | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/localHandlers/{onSaveMessage.js => onSaveMessage.ts} (58%) diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.ts similarity index 58% rename from apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js rename to apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.ts index b3a0947f8e30a..502c79f81a0b3 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.ts @@ -1,23 +1,24 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { SystemLogger } from '../../../../../server/lib/logger/system'; -export default async function handleOnSaveMessage(message, to) { +export default async function handleOnSaveMessage(this: any, message: IMessage, to: IRoom): Promise { let toIdentification = ''; // Direct message if (to.t === 'd') { - const subscriptions = Subscriptions.findByRoomId(to._id).toArray(); + const subscriptions = await Subscriptions.findByRoomId(to._id).toArray(); for await (const subscription of subscriptions) { if (subscription.u._id !== message.u._id) { const userData = await Users.findOne({ username: subscription.u.username }); if (userData) { - if (userData.profile && userData.profile.irc && userData.profile.irc.nick) { - toIdentification = userData.profile.irc.nick; + if ((userData as any).profile && (userData as any).profile.irc && (userData as any).profile.irc.nick) { + toIdentification = (userData as any).profile.irc.nick; } else { - toIdentification = userData.username; + toIdentification = userData.username as string; } } else { - toIdentification = subscription.u.username; + toIdentification = subscription.u.username as string; } } } From 682cd332573a8cf375f5a98ae8ce95822b6f9ee4 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:22:05 -0300 Subject: [PATCH 27/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js` to TypeScript --- .../{disconnected.js => disconnected.ts} | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{disconnected.js => disconnected.ts} (62%) diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.ts similarity index 62% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.ts index 3429a977fd1a6..cecb2d1e77ed2 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/disconnected.ts @@ -1,17 +1,26 @@ +import { UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; -export default async function handleQUIT(args) { +type QuitArgs = { + nick: string; +}; + +export default async function handleQUIT(args: QuitArgs): Promise { const user = await Users.findOne({ 'profile.irc.nick': args.nick, }); + if (!user) { + return; + } + await Users.updateOne( { _id: user._id }, { $set: { - status: 'offline', + status: UserStatus.OFFLINE, }, }, ); From aa2f0f05dc1cd9b40e84b627f1090260f9e6f88b Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:25:19 -0300 Subject: [PATCH 28/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js` to TypeScript --- .../peerHandlers/{joinedChannel.js => joinedChannel.ts} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{joinedChannel.js => joinedChannel.ts} (82%) diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.ts similarity index 82% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.ts index bb5053ffdd71c..aa45c31e4c08c 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/joinedChannel.ts @@ -3,8 +3,13 @@ import { Users, Rooms } from '@rocket.chat/models'; import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom'; import { createRoom } from '../../../../lib/server/functions/createRoom'; +type JoinedChannelArgs = { + nick: string; + roomName: string; +}; + // TODO doesn't seem to be used anywhere, remove -export default async function handleJoinedChannel(args) { +export default async function handleJoinedChannel(this: any, args: JoinedChannelArgs): Promise { const user = await Users.findOne({ 'profile.irc.nick': args.nick, }); From cca2dacda2a1afa214b62d542ffd6d2dea1c7d27 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:27:52 -0300 Subject: [PATCH 29/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js` to TypeScript --- .../peerHandlers/{leftChannel.js => leftChannel.ts} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{leftChannel.js => leftChannel.ts} (77%) diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.ts similarity index 77% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.ts index 9a65f35e5037d..10acfb4f29005 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/leftChannel.ts @@ -2,7 +2,12 @@ import { Users, Rooms } from '@rocket.chat/models'; import { removeUserFromRoom } from '../../../../lib/server/functions/removeUserFromRoom'; -export default async function handleLeftChannel(args) { +type LeftChannelArgs = { + nick: string; + roomName: string; +}; + +export default async function handleLeftChannel(this: any, args: LeftChannelArgs): Promise { const user = await Users.findOne({ 'profile.irc.nick': args.nick, }); From 6f90ba03b09731bbd1329458f81a113541f8b6d1 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:32:39 -0300 Subject: [PATCH 30/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js` to TypeScript --- .../peerHandlers/{nickChanged.js => nickChanged.ts} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{nickChanged.js => nickChanged.ts} (79%) diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.ts similarity index 79% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.ts index 96a8ebcb3dc90..b9f7b4f6db3e1 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/nickChanged.ts @@ -2,7 +2,12 @@ import { Users } from '@rocket.chat/models'; import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; -export default async function handleNickChanged(args) { +type NickChangedArgs = { + nick: string; + newNick: string; +}; + +export default async function handleNickChanged(this: any, args: NickChangedArgs): Promise { const user = await Users.findOne({ 'profile.irc.nick': args.nick, }); From 23ebbeef50a809d5b3bc590f8660a22ecab3e909 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:41:15 -0300 Subject: [PATCH 31/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js` to TypeScript --- .../{sentMessage.js => sentMessage.ts} | 28 +++++++++++++++---- .../lib/server/functions/createDirectRoom.ts | 4 +-- 2 files changed, 24 insertions(+), 8 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{sentMessage.js => sentMessage.ts} (61%) diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.ts similarity index 61% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.ts index c8298efab384c..608bfd180511b 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/sentMessage.ts @@ -1,3 +1,4 @@ +import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { Users, Rooms } from '@rocket.chat/models'; import { createDirectRoom } from '../../../../lib/server/functions/createDirectRoom'; @@ -9,7 +10,7 @@ import { sendMessage } from '../../../../lib/server/functions/sendMessage'; * * */ -const getDirectRoom = async (source, target) => { +const getDirectRoom = async (source: IUser, target: IUser): Promise => { const uids = [source._id, target._id]; const { _id, ...extraData } = await createDirectRoom([source, target]); @@ -17,18 +18,25 @@ const getDirectRoom = async (source, target) => { if (room) { return { t: 'd', - ...room, + ...(room as Omit), }; } return { _id, t: 'd', - ...extraData, - }; + ...(extraData as Omit), + } as IRoom; +}; + +type SentMessageArgs = { + nick: string; + roomName?: string; + recipientNick?: string; + message: string; }; -export default async function handleSentMessage(args) { +export default async function handleSentMessage(args: SentMessageArgs): Promise { const user = await Users.findOne({ 'profile.irc.nick': args.nick, }); @@ -37,7 +45,7 @@ export default async function handleSentMessage(args) { throw new Error(`Could not find a user with nick ${args.nick}`); } - let room; + let room: IRoom | null; if (args.roomName) { room = await Rooms.findOneByName(args.roomName); @@ -46,9 +54,17 @@ export default async function handleSentMessage(args) { 'profile.irc.nick': args.recipientNick, }); + if (!recipientUser) { + throw new Error(`Could not find recipient user with nick ${args.recipientNick}`); + } + room = await getDirectRoom(user, recipientUser); } + if (!room) { + throw new Error(`Could not find or create room`); + } + const message = { msg: args.message, ts: new Date(), diff --git a/apps/meteor/app/lib/server/functions/createDirectRoom.ts b/apps/meteor/app/lib/server/functions/createDirectRoom.ts index d4f439d7ae30a..7ee0cd0b483d1 100644 --- a/apps/meteor/app/lib/server/functions/createDirectRoom.ts +++ b/apps/meteor/app/lib/server/functions/createDirectRoom.ts @@ -41,7 +41,7 @@ const generateSubscription = ( export async function createDirectRoom( members: IUser[] | string[], roomExtraData: Partial = {}, - options: { + options?: { forceNew?: boolean; creator?: IUser['_id']; subscriptionExtra?: ISubscriptionExtraData; @@ -159,7 +159,7 @@ export async function createDirectRoom( for await (const member of membersWithPreferences) { const subscriptionStatus: Partial = - roomExtraData.federated && options.creator !== member._id && creatorUser + roomExtraData.federated && options!.creator !== member._id && creatorUser ? { status: 'INVITED', inviter: { From 71ba3d912e86a54a57354aec851227809dfd3b80 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:47:11 -0300 Subject: [PATCH 32/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js` to TypeScript --- .../{userRegistered.js => userRegistered.ts} | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/peerHandlers/{userRegistered.js => userRegistered.ts} (64%) diff --git a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.ts similarity index 64% rename from apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js rename to apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.ts index 5e04d7b79407b..3b694cb650f53 100644 --- a/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.js +++ b/apps/meteor/app/irc/server/irc-bridge/peerHandlers/userRegistered.ts @@ -1,10 +1,18 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import { UserStatus } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { notifyOnUserChange } from '../../../../lib/server/lib/notifyListener'; -export default async function handleUserRegistered(args) { +type UserRegisteredArgs = { + username: string; + nick: string; + hostname: string; +}; + +export default async function handleUserRegistered(this: any, args: UserRegisteredArgs): Promise { // Check if there is an user with the given username - let user = await Users.findOne({ + let user: IUser | null = await Users.findOne({ 'profile.irc.username': args.username, }); @@ -29,9 +37,12 @@ export default async function handleUserRegistered(args) { }, }; - user = await Users.create(userToInsert); + const insertResult = await Users.create(userToInsert as any); + user = await Users.findOne({ _id: insertResult.insertedId }); - void notifyOnUserChange({ id: user._id, clientAction: 'inserted', data: user }); + if (user) { + void notifyOnUserChange({ id: user._id, clientAction: 'inserted', data: user }); + } } else { // ...otherwise, log the user in and update the information this.log(`Logging in ${args.username} with nick: ${args.nick}`); @@ -40,7 +51,7 @@ export default async function handleUserRegistered(args) { { _id: user._id }, { $set: { - 'status': 'online', + 'status': UserStatus.ONLINE, 'profile.irc.nick': args.nick, 'profile.irc.username': args.username, 'profile.irc.hostname': args.hostname, From 5a30ef9ab39b0e019377aaa679240aceeb824c4e Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 22:54:34 -0300 Subject: [PATCH 33/80] refactor: convert `apps/meteor/app/irc/server/irc-bridge/index.js` to TypeScript --- .../server/irc-bridge/{index.js => index.ts} | 85 +++++++++++++------ 1 file changed, 59 insertions(+), 26 deletions(-) rename apps/meteor/app/irc/server/irc-bridge/{index.js => index.ts} (74%) diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.ts similarity index 74% rename from apps/meteor/app/irc/server/irc-bridge/index.js rename to apps/meteor/app/irc/server/irc-bridge/index.ts index 25f6b5b9f0a29..2e11d1deee06d 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.ts @@ -1,3 +1,4 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { Settings } from '@rocket.chat/models'; import moment from 'moment'; @@ -16,6 +17,25 @@ import * as peerCommandHandlers from './peerHandlers'; const logger = new Logger('IRC Bridge'); const queueLogger = logger.section('Queue'); +type QueueItem = { + from: string; + command: string; + parameters: any[]; +}; + +type BridgeConfig = { + server: { + protocol: any; + [key: string]: any; + }; + [key: string]: any; +}; + +type PeerCommand = { + identifier: string; + args: any; +}; + let removed = false; const updateLastPing = withThrottling({ wait: 10_000 })(() => { if (removed) { @@ -33,7 +53,19 @@ const updateLastPing = withThrottling({ wait: 10_000 })(() => { }); class Bridge { - constructor(config) { + config: BridgeConfig; + + loggedInUsers: string[]; + + server: any; + + queue: Queue; + + queueTimeout: number; + + initTime?: Date; + + constructor(config: BridgeConfig) { // General this.config = config; @@ -41,7 +73,7 @@ class Bridge { this.loggedInUsers = []; // Server - const Server = servers[this.config.server.protocol]; + const Server = (servers as any)[this.config.server.protocol]; this.server = new Server(this.config); @@ -53,14 +85,14 @@ class Bridge { this.queueTimeout = 5; } - async init() { + async init(): Promise { this.initTime = new Date(); removed = false; this.loggedInUsers = []; const lastPing = await Settings.findOneById('IRC_Bridge_Last_Ping'); if (lastPing) { - if (Math.abs(moment(lastPing.value).diff()) < 1000 * 30) { + if (Math.abs(moment(lastPing.value as any).diff(moment())) < 1000 * 30) { this.log('Not trying to connect.'); this.remove(); return; @@ -74,15 +106,15 @@ class Bridge { this.server.on('registered', () => { this.logQueue('Starting...'); - this.runQueue(); + void this.runQueue(); }); } - stop() { + stop(): void { this.server.disconnect(); } - remove() { + remove(): void { this.log('Removing current connection.'); removed = true; this.server = null; @@ -92,12 +124,12 @@ class Bridge { /** * Log helper */ - log(message) { + log(message: string): void { // TODO logger: debug? logger.info(message); } - logQueue(message) { + logQueue(message: string | Record): void { // TODO logger: debug? queueLogger.info(message); } @@ -109,17 +141,17 @@ class Bridge { * * */ - onMessageReceived(from, command, ...parameters) { + onMessageReceived(from: string, command: string, ...parameters: any[]): void { this.queue.enqueue({ from, command, parameters }); } - async runQueue() { + async runQueue(): Promise { if (!this.server) { return; } - const lastResetTime = Settings.findOneById('IRC_Bridge_Reset_Time'); - if (lastResetTime && lastResetTime.value > this.initTime) { + const lastResetTime = await Settings.findOneById('IRC_Bridge_Reset_Time'); + if (lastResetTime?.value && lastResetTime.value > this.initTime!) { this.stop(); this.remove(); return; @@ -129,11 +161,12 @@ class Bridge { // If it is empty, skip and keep the queue going if (this.queue.isEmpty()) { - return setTimeout(this.runQueue.bind(this), this.queueTimeout); + setTimeout(this.runQueue.bind(this), this.queueTimeout); + return; } // Get the command - const item = this.queue.dequeue(); + const item = this.queue.dequeue() as QueueItem; this.logQueue({ msg: 'Processing command from source', command: item.command, from: item.from }); @@ -142,22 +175,22 @@ class Bridge { // Handle the command accordingly switch (item.from) { case 'local': - if (!localCommandHandlers[item.command]) { + if (!(localCommandHandlers as any)[item.command]) { throw new Error(`Could not find handler for local:${item.command}`); } - await localCommandHandlers[item.command].apply(this, item.parameters); + await (localCommandHandlers as any)[item.command].apply(this, item.parameters); break; case 'peer': - if (!peerCommandHandlers[item.command]) { + if (!(peerCommandHandlers as any)[item.command]) { throw new Error(`Could not find handler for peer:${item.command}`); } - await peerCommandHandlers[item.command].apply(this, item.parameters); + await (peerCommandHandlers as any)[item.command].apply(this, item.parameters); break; } } catch (e) { - this.logQueue(e); + this.logQueue(e as any); } // Keep the queue going @@ -171,8 +204,8 @@ class Bridge { * * */ - setupPeerHandlers() { - this.server.on('peerCommand', (cmd) => { + setupPeerHandlers(): void { + this.server.on('peerCommand', (cmd: PeerCommand) => { this.onMessageReceived('peer', cmd.identifier, cmd.args); }); } @@ -184,7 +217,7 @@ class Bridge { * * */ - setupLocalHandlers() { + setupLocalHandlers(): void { // Auth callbacks.add('afterValidateLogin', this.onMessageReceived.bind(this, 'local', 'onLogin'), callbacks.priority.LOW, 'irc-on-login'); callbacks.add( @@ -212,7 +245,7 @@ class Bridge { // Chatting callbacks.add( 'afterSaveMessage', - (message, { room }) => this.onMessageReceived('local', 'onSaveMessage', message, room), + (message: IMessage, { room }: { room: IRoom }) => this.onMessageReceived('local', 'onSaveMessage', message, room), callbacks.priority.LOW, 'irc-on-save-message', ); @@ -220,7 +253,7 @@ class Bridge { afterLogoutCleanUpCallback.add(this.onMessageReceived.bind(this, 'local', 'onLogout'), callbacks.priority.LOW, 'irc-on-logout'); } - removeLocalHandlers() { + removeLocalHandlers(): void { callbacks.remove('afterValidateLogin', 'irc-on-login'); callbacks.remove('afterCreateUser', 'irc-on-create-user'); callbacks.remove('afterCreateChannel', 'irc-on-create-channel'); @@ -231,7 +264,7 @@ class Bridge { afterLogoutCleanUpCallback.remove('irc-on-logout'); } - sendCommand(command, parameters) { + sendCommand(command: string, parameters: any): void { this.server.emit('onReceiveFromLocal', command, parameters); } } From d18a25b822070684f302dab1b5c2fd2f37e8cf4e Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 23:13:57 -0300 Subject: [PATCH 34/80] refactor: convert `apps/meteor/app/irc/server/servers/RFC2813/codes.js` to TypeScript --- .../server/servers/RFC2813/{codes.js => codes.ts} | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) rename apps/meteor/app/irc/server/servers/RFC2813/{codes.js => codes.ts} (97%) diff --git a/apps/meteor/app/irc/server/servers/RFC2813/codes.js b/apps/meteor/app/irc/server/servers/RFC2813/codes.ts similarity index 97% rename from apps/meteor/app/irc/server/servers/RFC2813/codes.js rename to apps/meteor/app/irc/server/servers/RFC2813/codes.ts index a9f3d0ee55a96..b781f7405ee2e 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/codes.js +++ b/apps/meteor/app/irc/server/servers/RFC2813/codes.ts @@ -3,7 +3,14 @@ * by https://github.com/martynsmith */ -module.exports = { +type CodeInfo = { + name: string; + type: 'reply' | 'error'; +}; + +type Codes = Record; + +const codes = { '001': { name: 'rpl_welcome', type: 'reply', @@ -516,4 +523,6 @@ module.exports = { name: 'err_usersdontmatch', type: 'error', }, -}; +} satisfies Codes; + +export default codes; From 1ee93419794e8e55cf03699ad19ccc541d3d2e27 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 23:18:18 -0300 Subject: [PATCH 35/80] refactor: convert `apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.js` to TypeScript --- ...mandHandlers.js => peerCommandHandlers.ts} | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) rename apps/meteor/app/irc/server/servers/RFC2813/{peerCommandHandlers.js => peerCommandHandlers.ts} (61%) diff --git a/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.js b/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts similarity index 61% rename from apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.js rename to apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts index 13b5b20084533..5ea7f94c60166 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.js +++ b/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts @@ -1,18 +1,44 @@ -function PASS() { +type ParsedMessage = { + prefix?: string; + command: string; + args: string[]; + nick?: string; +}; + +type CommandResult = { + identifier: string; + args: Record; +}; + +type RFC2813Context = { + log: (message: string) => void; + registerSteps: string[]; + serverPrefix: string | null; + isRegistered: boolean; + emit: (event: string) => void; + write: (command: { prefix?: string; command: string; parameters?: string[] }) => void; + config: { + server: { + name: string; + }; + }; +}; + +function PASS(this: RFC2813Context): void { this.log('Received PASS command, continue registering...'); this.registerSteps.push('PASS'); } -function SERVER(parsedMessage) { +function SERVER(this: RFC2813Context, parsedMessage: ParsedMessage): void { this.log('Received SERVER command, waiting for first PING...'); - this.serverPrefix = parsedMessage.prefix; + this.serverPrefix = parsedMessage.prefix || null; this.registerSteps.push('SERVER'); } -function PING() { +function PING(this: RFC2813Context): void { if (!this.isRegistered && this.registerSteps.length === 2) { this.log('Received first PING command, server is registered!'); @@ -28,8 +54,8 @@ function PING() { }); } -function NICK(parsedMessage) { - let command; +function NICK(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult { + let command: CommandResult; // Check if the message comes from the server, // which means it is a new user @@ -57,7 +83,7 @@ function NICK(parsedMessage) { return command; } -function JOIN(parsedMessage) { +function JOIN(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult { const command = { identifier: 'joinedChannel', args: { @@ -69,7 +95,7 @@ function JOIN(parsedMessage) { return command; } -function PART(parsedMessage) { +function PART(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult { const command = { identifier: 'leftChannel', args: { @@ -81,8 +107,8 @@ function PART(parsedMessage) { return command; } -function PRIVMSG(parsedMessage) { - const command = { +function PRIVMSG(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult { + const command: CommandResult = { identifier: 'sentMessage', args: { nick: parsedMessage.prefix, @@ -99,7 +125,7 @@ function PRIVMSG(parsedMessage) { return command; } -function QUIT(parsedMessage) { +function QUIT(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult { const command = { identifier: 'disconnected', args: { From 81c65a43e3cc048deca4ba224a04ecf539c1ba93 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 23:21:28 -0300 Subject: [PATCH 36/80] refactor: convert `apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.js` to TypeScript --- .../servers/RFC2813/localCommandHandlers.js | 111 ---------- .../servers/RFC2813/localCommandHandlers.ts | 195 ++++++++++++++++++ 2 files changed, 195 insertions(+), 111 deletions(-) delete mode 100644 apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.js create mode 100644 apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts diff --git a/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.js b/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.js deleted file mode 100644 index 176da11e27b00..0000000000000 --- a/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.js +++ /dev/null @@ -1,111 +0,0 @@ -function registerUser(parameters) { - const { - name, - profile: { - irc: { nick, username }, - }, - } = parameters; - - this.write({ - prefix: this.config.server.name, - command: 'NICK', - parameters: [nick, 1, username, 'irc.rocket.chat', 1, '+i'], - trailer: name, - }); -} - -function joinChannel(parameters) { - const { - room: { name: roomName }, - user: { - profile: { - irc: { nick }, - }, - }, - } = parameters; - - this.write({ - prefix: this.config.server.name, - command: 'NJOIN', - parameters: [`#${roomName}`], - trailer: nick, - }); -} - -function joinedChannel(parameters, handler) { - const roomName = parameters.room?.name; - const nick = parameters.user?.profile?.irc?.nick; - - if (!roomName) { - handler.log('Skipping room with no name.'); - return; - } - - if (!nick) { - handler.log('Skipping user with no irc nick.'); - return; - } - - this.write({ - prefix: nick, - command: 'JOIN', - parameters: [`#${roomName}`], - }); -} - -function leftChannel(parameters) { - const { - room: { name: roomName }, - user: { - profile: { - irc: { nick }, - }, - }, - } = parameters; - - this.write({ - prefix: nick, - command: 'PART', - parameters: [`#${roomName}`], - }); -} - -function sentMessage(parameters) { - const { - user: { - profile: { - irc: { nick }, - }, - }, - to, - message, - } = parameters; - - // eslint-disable-next-line no-control-regex - const lines = message.toString().split(/\r\n|\r|\n|\u0007/); - for (const line of lines) { - this.write({ - prefix: nick, - command: 'PRIVMSG', - parameters: [to], - trailer: line, - }); - } -} - -function disconnected(parameters) { - const { - user: { - profile: { - irc: { nick }, - }, - }, - } = parameters; - - this.write({ - prefix: nick, - command: 'QUIT', - }); -} - -export default { registerUser, joinChannel, joinedChannel, leftChannel, sentMessage, disconnected }; diff --git a/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts b/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts new file mode 100644 index 0000000000000..3a7c97d1658ec --- /dev/null +++ b/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts @@ -0,0 +1,195 @@ +type RFC2813Context = { + write: (command: { prefix?: string; command: string; parameters?: string[]; trailer?: string }) => void; + config: { + server: { + name: string; + }; + }; +}; + +type RegisterUserParameters = { + name: string; + profile: { + irc: { + nick: string; + username: string; + }; + }; +}; + +type JoinChannelParameters = { + room: { + name: string; + }; + user: { + profile: { + irc: { + nick: string; + }; + }; + }; +}; + +type JoinedChannelParameters = { + room?: { + name?: string; + }; + user?: { + profile?: { + irc?: { + nick?: string; + }; + }; + }; +}; + +type LeftChannelParameters = { + room: { + name: string; + }; + user: { + profile: { + irc: { + nick: string; + }; + }; + }; +}; + +type SentMessageParameters = { + user: { + profile: { + irc: { + nick: string; + }; + }; + }; + to: string; + message: string; +}; + +type DisconnectedParameters = { + user: { + profile: { + irc: { + nick: string; + }; + }; + }; +}; + +type Handler = { + log: (message: string) => void; +}; + +function registerUser(this: RFC2813Context, parameters: RegisterUserParameters): void { + const { + name, + profile: { + irc: { nick, username }, + }, + } = parameters; + + this.write({ + prefix: this.config.server.name, + command: 'NICK', + parameters: [nick, '1', username, 'irc.rocket.chat', '1', '+i'], + trailer: name, + }); +} + +function joinChannel(this: RFC2813Context, parameters: JoinChannelParameters): void { + const { + room: { name: roomName }, + user: { + profile: { + irc: { nick }, + }, + }, + } = parameters; + + this.write({ + prefix: this.config.server.name, + command: 'NJOIN', + parameters: [`#${roomName}`], + trailer: nick, + }); +} + +function joinedChannel(this: RFC2813Context, parameters: JoinedChannelParameters, handler: Handler): void { + const roomName = parameters.room?.name; + const nick = parameters.user?.profile?.irc?.nick; + + if (!roomName) { + handler.log('Skipping room with no name.'); + return; + } + + if (!nick) { + handler.log('Skipping user with no irc nick.'); + return; + } + + this.write({ + prefix: nick, + command: 'JOIN', + parameters: [`#${roomName}`], + }); +} + +function leftChannel(this: RFC2813Context, parameters: LeftChannelParameters): void { + const { + room: { name: roomName }, + user: { + profile: { + irc: { nick }, + }, + }, + } = parameters; + + this.write({ + prefix: nick, + command: 'PART', + parameters: [`#${roomName}`], + }); +} + +function sentMessage(this: RFC2813Context, parameters: SentMessageParameters): void { + const { + user: { + profile: { + irc: { nick }, + }, + }, + to, + message, + } = parameters; + + // eslint-disable-next-line no-control-regex + const lines = message.toString().split(/\r\n|\r|\n|\u0007/); + for (const line of lines) { + this.write({ + prefix: nick, + command: 'PRIVMSG', + parameters: [to], + trailer: line, + }); + } +} + +function disconnected(this: RFC2813Context, parameters: DisconnectedParameters): void { + const { + user: { + profile: { + irc: { nick }, + }, + }, + } = parameters; + + this.write({ + prefix: nick, + command: 'QUIT', + }); +} + +export default { registerUser, joinChannel, joinedChannel, leftChannel, sentMessage, disconnected }; From 8e0f21e654db852bcf69b570687ba9495e6ab348 Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 23:26:50 -0300 Subject: [PATCH 37/80] refactor: convert `apps/meteor/app/irc/server/servers/RFC2813/parseMessage.js` to TypeScript --- .../{parseMessage.js => parseMessage.ts} | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) rename apps/meteor/app/irc/server/servers/RFC2813/{parseMessage.js => parseMessage.ts} (59%) diff --git a/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.js b/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts similarity index 59% rename from apps/meteor/app/irc/server/servers/RFC2813/parseMessage.js rename to apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts index 15e7a616b87fc..4a91f9fdbfcf1 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.js +++ b/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts @@ -3,7 +3,19 @@ * by https://github.com/martynsmith */ -const replyFor = require('./codes'); +import replyFor from './codes'; + +type ParsedMessage = { + prefix?: string; + nick?: string; + user?: string; + host?: string; + server?: string; + command: string; + rawCommand: string; + commandType: string; + args: string[]; +}; /** * parseMessage(line, stripColors) @@ -13,9 +25,9 @@ const replyFor = require('./codes'); * @param {String} line Raw message from IRC server. * @return {Object} A parsed message object. */ -module.exports = function parseMessage(line) { - const message = {}; - let match; +export default function parseMessage(line: string): ParsedMessage { + const message: Partial = {}; + let match: RegExpMatchArray | null; // Parse prefix match = line.match(/^:([^ ]+) +/); @@ -34,25 +46,25 @@ module.exports = function parseMessage(line) { // Parse command match = line.match(/^([^ ]+) */); - message.command = match[1]; - message.rawCommand = match[1]; + message.command = match![1]; + message.rawCommand = match![1]; message.commandType = 'normal'; line = line.replace(/^[^ ]+ +/, ''); - if (replyFor[message.rawCommand]) { - message.command = replyFor[message.rawCommand].name; - message.commandType = replyFor[message.rawCommand].type; + if (replyFor[message.rawCommand as keyof typeof replyFor]) { + message.command = replyFor[message.rawCommand as keyof typeof replyFor].name; + message.commandType = replyFor[message.rawCommand as keyof typeof replyFor].type; } message.args = []; - let middle; - let trailing; + let middle: string; + let trailing: string | undefined; // Parse parameters if (line.search(/^:|\s+:/) !== -1) { match = line.match(/(.*?)(?:^:|\s+:)(.*)/); - middle = match[1].trimRight(); - trailing = match[2]; + middle = match![1].trimEnd(); + trailing = match![2]; } else { middle = line; } @@ -65,5 +77,5 @@ module.exports = function parseMessage(line) { message.args.push(trailing); } - return message; -}; + return message as ParsedMessage; +} From 0075c6454b58d4cda941d328bb8776d339f01cfb Mon Sep 17 00:00:00 2001 From: Tasso Date: Wed, 28 Jan 2026 23:36:49 -0300 Subject: [PATCH 38/80] refactor: convert `apps/meteor/app/irc/server/servers/RFC2813/index.js` to TypeScript --- .../servers/RFC2813/{index.js => index.ts} | 85 +++++++++++++------ 1 file changed, 60 insertions(+), 25 deletions(-) rename apps/meteor/app/irc/server/servers/RFC2813/{index.js => index.ts} (64%) diff --git a/apps/meteor/app/irc/server/servers/RFC2813/index.js b/apps/meteor/app/irc/server/servers/RFC2813/index.ts similarity index 64% rename from apps/meteor/app/irc/server/servers/RFC2813/index.js rename to apps/meteor/app/irc/server/servers/RFC2813/index.ts index 531338d939827..ce1ce7a0e70cb 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/index.js +++ b/apps/meteor/app/irc/server/servers/RFC2813/index.ts @@ -1,6 +1,5 @@ import { EventEmitter } from 'events'; import net from 'net'; -import util from 'util'; import { Logger } from '@rocket.chat/logger'; @@ -10,8 +9,50 @@ import peerCommandHandlers from './peerCommandHandlers'; const logger = new Logger('IRC Server'); -class RFC2813 { - constructor(config) { +type Config = { + server: { + name: string; + host: string; + port: number; + description: string; + }; + passwords: { + local: string; + }; +}; + +type Command = { + prefix?: string; + command: string; + parameters?: string[]; + trailer?: string; +}; + +// eslint-disable-next-line @typescript-eslint/naming-convention +interface RFC2813 extends EventEmitter { + on(event: 'onReceiveFromLocal', listener: (command: string, parameters: any) => void): this; + on(event: 'peerCommand', listener: (command: any) => void): this; + on(event: string | symbol, listener: (...args: any[]) => void): this; + emit(event: 'peerCommand', command: any): boolean; + emit(event: 'onReceiveFromLocal', command: string, parameters: any): boolean; + emit(event: string | symbol, ...args: any[]): boolean; +} + +class RFC2813 extends EventEmitter { + config: Config; + + registerSteps: string[]; + + isRegistered: boolean; + + serverPrefix: string | null; + + receiveBuffer: Buffer; + + socket?: net.Socket; + + constructor(config: Config) { + super(); this.config = config; // Hold registered state @@ -28,7 +69,7 @@ class RFC2813 { /** * Setup socket */ - setupSocket() { + setupSocket(): void { // Setup socket this.socket = new net.Socket(); this.socket.setNoDelay(); @@ -49,7 +90,7 @@ class RFC2813 { /** * Log helper */ - log(message) { + log(message: string | Record): void { // TODO logger: debug? if (typeof message === 'string') { logger.info({ msg: message }); @@ -62,7 +103,7 @@ class RFC2813 { /** * Connect */ - register() { + register(): void { this.log({ msg: 'Connecting to IRC server', host: this.config.server.host, @@ -73,13 +114,13 @@ class RFC2813 { this.setupSocket(); } - this.socket.connect(this.config.server.port, this.config.server.host); + this.socket!.connect(this.config.server.port, this.config.server.host); } /** * Disconnect */ - disconnect() { + disconnect(): void { this.log('Disconnecting from server.'); if (this.socket) { @@ -93,7 +134,7 @@ class RFC2813 { /** * Setup the server connection */ - onConnect() { + onConnect(): void { this.log('Connected! Registering as server...'); this.write({ @@ -111,7 +152,7 @@ class RFC2813 { /** * Sends a command message through the socket */ - write(command) { + write(command: Command): boolean { let buffer = command.prefix ? `:${command.prefix} ` : ''; buffer += command.command; @@ -125,7 +166,7 @@ class RFC2813 { this.log({ msg: 'Sending Command', buffer }); - return this.socket.write(`${buffer}\r\n`); + return this.socket!.write(`${buffer}\r\n`); } /** @@ -135,9 +176,9 @@ class RFC2813 { * * */ - onReceiveFromPeer(chunk) { + onReceiveFromPeer(chunk: string | Buffer): void { if (typeof chunk === 'string') { - this.receiveBuffer += chunk; + this.receiveBuffer = Buffer.concat([this.receiveBuffer, Buffer.from(chunk)]); } else { this.receiveBuffer = Buffer.concat([this.receiveBuffer, chunk]); } @@ -156,12 +197,12 @@ class RFC2813 { if (line.length && !line.startsWith('a')) { const parsedMessage = parseMessage(line); - if (peerCommandHandlers[parsedMessage.command]) { + if (peerCommandHandlers[parsedMessage.command as keyof typeof peerCommandHandlers]) { this.log({ msg: 'Handling peer message', line }); - const command = peerCommandHandlers[parsedMessage.command].call(this, parsedMessage); + const command = peerCommandHandlers[parsedMessage.command as keyof typeof peerCommandHandlers].call(this, parsedMessage); - if (command) { + if (command !== undefined) { this.log({ msg: 'Emitting peer command to local', command }); this.emit('peerCommand', command); } @@ -173,23 +214,17 @@ class RFC2813 { } /** - * - * * Local message handling - * - * */ - onReceiveFromLocal(command, parameters) { - if (localCommandHandlers[command]) { + onReceiveFromLocal(command: string, parameters: any): void { + if (localCommandHandlers[command as keyof typeof localCommandHandlers]) { this.log({ msg: 'Handling local command', command }); - localCommandHandlers[command].call(this, parameters, this); + localCommandHandlers[command as keyof typeof localCommandHandlers].call(this, parameters, this); } else { this.log({ msg: 'Unhandled local command', command }); } } } -util.inherits(RFC2813, EventEmitter); - export default RFC2813; From 9d7b382230105c10ed88fe3c73f941a02824f902 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 00:49:43 -0300 Subject: [PATCH 39/80] refactor: normalize types on `apps/meteor/app/irc/server/servers/RFC2813` --- .../app/irc/server/servers/RFC2813/index.ts | 32 +++----------- .../servers/RFC2813/localCommandHandlers.ts | 11 +---- .../server/servers/RFC2813/parseMessage.ts | 20 +++------ .../servers/RFC2813/peerCommandHandlers.ts | 40 +++++------------ .../app/irc/server/servers/RFC2813/types.ts | 44 +++++++++++++++++++ apps/meteor/app/irc/server/servers/index.ts | 4 +- 6 files changed, 70 insertions(+), 81 deletions(-) create mode 100644 apps/meteor/app/irc/server/servers/RFC2813/types.ts diff --git a/apps/meteor/app/irc/server/servers/RFC2813/index.ts b/apps/meteor/app/irc/server/servers/RFC2813/index.ts index ce1ce7a0e70cb..6e7118b7b6037 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/index.ts +++ b/apps/meteor/app/irc/server/servers/RFC2813/index.ts @@ -3,31 +3,13 @@ import net from 'net'; import { Logger } from '@rocket.chat/logger'; -import localCommandHandlers from './localCommandHandlers'; -import parseMessage from './parseMessage'; -import peerCommandHandlers from './peerCommandHandlers'; +import * as localCommandHandlers from './localCommandHandlers'; +import { parseMessage } from './parseMessage'; +import * as peerCommandHandlers from './peerCommandHandlers'; +import type { Config, Command } from './types'; const logger = new Logger('IRC Server'); -type Config = { - server: { - name: string; - host: string; - port: number; - description: string; - }; - passwords: { - local: string; - }; -}; - -type Command = { - prefix?: string; - command: string; - parameters?: string[]; - trailer?: string; -}; - // eslint-disable-next-line @typescript-eslint/naming-convention interface RFC2813 extends EventEmitter { on(event: 'onReceiveFromLocal', listener: (command: string, parameters: any) => void): this; @@ -197,10 +179,10 @@ class RFC2813 extends EventEmitter { if (line.length && !line.startsWith('a')) { const parsedMessage = parseMessage(line); - if (peerCommandHandlers[parsedMessage.command as keyof typeof peerCommandHandlers]) { + if (peerCommandHandlers[parsedMessage.command]) { this.log({ msg: 'Handling peer message', line }); - const command = peerCommandHandlers[parsedMessage.command as keyof typeof peerCommandHandlers].call(this, parsedMessage); + const command = peerCommandHandlers[parsedMessage.command].call(this, parsedMessage); if (command !== undefined) { this.log({ msg: 'Emitting peer command to local', command }); @@ -227,4 +209,4 @@ class RFC2813 extends EventEmitter { } } -export default RFC2813; +export { RFC2813 }; diff --git a/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts b/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts index 3a7c97d1658ec..f483b4f7f4592 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts +++ b/apps/meteor/app/irc/server/servers/RFC2813/localCommandHandlers.ts @@ -1,11 +1,4 @@ -type RFC2813Context = { - write: (command: { prefix?: string; command: string; parameters?: string[]; trailer?: string }) => void; - config: { - server: { - name: string; - }; - }; -}; +import type { RFC2813Context } from './types'; type RegisterUserParameters = { name: string; @@ -192,4 +185,4 @@ function disconnected(this: RFC2813Context, parameters: DisconnectedParameters): }); } -export default { registerUser, joinChannel, joinedChannel, leftChannel, sentMessage, disconnected }; +export { registerUser, joinChannel, joinedChannel, leftChannel, sentMessage, disconnected }; diff --git a/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts b/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts index 4a91f9fdbfcf1..e2253fc5ad996 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts +++ b/apps/meteor/app/irc/server/servers/RFC2813/parseMessage.ts @@ -4,18 +4,8 @@ */ import replyFor from './codes'; - -type ParsedMessage = { - prefix?: string; - nick?: string; - user?: string; - host?: string; - server?: string; - command: string; - rawCommand: string; - commandType: string; - args: string[]; -}; +import type * as peerCommandHandlers from './peerCommandHandlers'; +import type { ParsedMessage } from './types'; /** * parseMessage(line, stripColors) @@ -25,7 +15,7 @@ type ParsedMessage = { * @param {String} line Raw message from IRC server. * @return {Object} A parsed message object. */ -export default function parseMessage(line: string): ParsedMessage { +export function parseMessage(line: string): ParsedMessage { const message: Partial = {}; let match: RegExpMatchArray | null; @@ -46,13 +36,13 @@ export default function parseMessage(line: string): ParsedMessage { // Parse command match = line.match(/^([^ ]+) */); - message.command = match![1]; + message.command = match![1] as keyof typeof peerCommandHandlers; message.rawCommand = match![1]; message.commandType = 'normal'; line = line.replace(/^[^ ]+ +/, ''); if (replyFor[message.rawCommand as keyof typeof replyFor]) { - message.command = replyFor[message.rawCommand as keyof typeof replyFor].name; + message.command = replyFor[message.rawCommand as keyof typeof replyFor].name as keyof typeof peerCommandHandlers; message.commandType = replyFor[message.rawCommand as keyof typeof replyFor].type; } diff --git a/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts b/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts index 5ea7f94c60166..1ce504a4b53ae 100644 --- a/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts +++ b/apps/meteor/app/irc/server/servers/RFC2813/peerCommandHandlers.ts @@ -1,44 +1,24 @@ -type ParsedMessage = { - prefix?: string; - command: string; - args: string[]; - nick?: string; -}; - -type CommandResult = { - identifier: string; - args: Record; -}; - -type RFC2813Context = { - log: (message: string) => void; - registerSteps: string[]; - serverPrefix: string | null; - isRegistered: boolean; - emit: (event: string) => void; - write: (command: { prefix?: string; command: string; parameters?: string[] }) => void; - config: { - server: { - name: string; - }; - }; -}; +import type { ParsedMessage, CommandResult, RFC2813Context } from './types'; -function PASS(this: RFC2813Context): void { +function PASS(this: RFC2813Context): undefined { this.log('Received PASS command, continue registering...'); this.registerSteps.push('PASS'); + + return undefined; } -function SERVER(this: RFC2813Context, parsedMessage: ParsedMessage): void { +function SERVER(this: RFC2813Context, parsedMessage: ParsedMessage): undefined { this.log('Received SERVER command, waiting for first PING...'); this.serverPrefix = parsedMessage.prefix || null; this.registerSteps.push('SERVER'); + + return undefined; } -function PING(this: RFC2813Context): void { +function PING(this: RFC2813Context): undefined { if (!this.isRegistered && this.registerSteps.length === 2) { this.log('Received first PING command, server is registered!'); @@ -52,6 +32,8 @@ function PING(this: RFC2813Context): void { command: 'PONG', parameters: [this.config.server.name], }); + + return undefined; } function NICK(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult { @@ -136,4 +118,4 @@ function QUIT(this: RFC2813Context, parsedMessage: ParsedMessage): CommandResult return command; } -export default { PASS, SERVER, PING, NICK, JOIN, PART, PRIVMSG, QUIT }; +export { PASS, SERVER, PING, NICK, JOIN, PART, PRIVMSG, QUIT }; diff --git a/apps/meteor/app/irc/server/servers/RFC2813/types.ts b/apps/meteor/app/irc/server/servers/RFC2813/types.ts new file mode 100644 index 0000000000000..7afe389cdbb5e --- /dev/null +++ b/apps/meteor/app/irc/server/servers/RFC2813/types.ts @@ -0,0 +1,44 @@ +/** + * Shared types for RFC2813 IRC server implementation + */ + +import type { RFC2813 } from './index'; +import type * as peerCommandHandlers from './peerCommandHandlers'; + +export type ParsedMessage = { + prefix?: string; + nick?: string; + user?: string; + host?: string; + server?: string; + command: keyof typeof peerCommandHandlers; + rawCommand: string; + commandType: string; + args: string[]; +}; + +export type CommandResult = { + identifier: string; + args: Record; +}; + +export type Command = { + prefix?: string; + command: string; + parameters?: string[]; + trailer?: string; +}; + +export type Config = { + server: { + name: string; + host: string; + port: number; + description: string; + }; + passwords: { + local: string; + }; +}; + +export type RFC2813Context = RFC2813; diff --git a/apps/meteor/app/irc/server/servers/index.ts b/apps/meteor/app/irc/server/servers/index.ts index 6ab8fd6b45940..baafde6963c53 100644 --- a/apps/meteor/app/irc/server/servers/index.ts +++ b/apps/meteor/app/irc/server/servers/index.ts @@ -1,3 +1 @@ -import RFC2813 from './RFC2813'; - -export { RFC2813 }; +export { RFC2813 } from './RFC2813'; From 0f0282e79d494665bccfe47d1eb391b536c01b55 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 01:00:41 -0300 Subject: [PATCH 40/80] refactor: convert `apps/meteor/app/lib/server/startup/rateLimiter.js` to TypeScript --- .../{rateLimiter.js => rateLimiter.ts} | 145 ++++++++++-------- 1 file changed, 82 insertions(+), 63 deletions(-) rename apps/meteor/app/lib/server/startup/{rateLimiter.js => rateLimiter.ts} (57%) diff --git a/apps/meteor/app/lib/server/startup/rateLimiter.js b/apps/meteor/app/lib/server/startup/rateLimiter.ts similarity index 57% rename from apps/meteor/app/lib/server/startup/rateLimiter.js rename to apps/meteor/app/lib/server/startup/rateLimiter.ts index 5a312f4520d46..a7ea390adbb98 100644 --- a/apps/meteor/app/lib/server/startup/rateLimiter.js +++ b/apps/meteor/app/lib/server/startup/rateLimiter.ts @@ -10,61 +10,78 @@ import { settings } from '../../../settings/server'; const logger = new Logger('RateLimiter'); -const slowDownRate = parseInt(process.env.RATE_LIMITER_SLOWDOWN_RATE); +const slowDownRate = parseInt(process.env.RATE_LIMITER_SLOWDOWN_RATE || '0'); -const rateLimiterConsoleLog = ({ msg, reply, input }) => { +type RateLimiterInput = { + connectionId: string; + broadcastAuth?: boolean; + userId?: string; + clientAddress?: string; + type: string; + name: string; +}; + +type RateLimiterReply = { + allowed: boolean; + timeToReset: number; + numInvocationsLeft: number; + numInvocationsExceeded?: number; +}; + +const rateLimiterConsoleLog = ({ msg, reply, input }: { msg: string; reply: RateLimiterReply; input: RateLimiterInput }): void => { console.warn('DDP RATE LIMIT:', msg); console.warn(JSON.stringify({ reply, input }, null, 2)); }; -const rateLimiterLogger = ({ msg, reply, input }) => logger.info({ msg, reply, input }); +const rateLimiterLogger = ({ msg, reply, input }: { msg: string; reply: RateLimiterReply; input: RateLimiterInput }): void => + logger.info({ msg, reply, input }); const rateLimiterLog = String(process.env.RATE_LIMITER_LOGGER) === 'console' ? rateLimiterConsoleLog : rateLimiterLogger; // Get initial set of names already registered for rules const names = new Set( - Object.values(DDPRateLimiter.printRules()) - .map((rule) => rule._matchers) - .filter((match) => typeof match.name === 'string') - .map((match) => match.name), + Object.values((DDPRateLimiter as any).printRules()) + .map((rule: any) => rule._matchers) + .filter((match: any) => typeof match.name === 'string') + .map((match: any) => match.name), ); // Override the addRule to save new names added after this point const { addRule } = DDPRateLimiter; -DDPRateLimiter.addRule = (matcher, calls, time, callback) => { +DDPRateLimiter.addRule = (matcher: any, calls: number, time: number, callback?: any): string => { if (matcher && typeof matcher.name === 'string') { names.add(matcher.name); } - return addRule.call(DDPRateLimiter, matcher, calls, time, callback); + return (addRule as any).call(DDPRateLimiter, matcher, calls, time, callback); }; -const { _increment } = DDPRateLimiter; -DDPRateLimiter._increment = function (input) { +const { _increment } = DDPRateLimiter as any; +(DDPRateLimiter as any)._increment = function (input: RateLimiterInput) { const session = Meteor.server.sessions.get(input.connectionId); - input.broadcastAuth = (session && session.connectionHandle && session.connectionHandle.broadcastAuth) === true; + input.broadcastAuth = (session && (session as any).connectionHandle && (session as any).connectionHandle.broadcastAuth) === true; return _increment.call(DDPRateLimiter, input); }; // Need to override the meteor's code duo to a problem with the callback reply // being shared among all matchs -RateLimiter.prototype.check = function (input) { +(RateLimiter.prototype as any).check = function (input: RateLimiterInput): RateLimiterReply { // ==== BEGIN OVERRIDE ==== const session = Meteor.server.sessions.get(input.connectionId); - input.broadcastAuth = (session && session.connectionHandle && session.connectionHandle.broadcastAuth) === true; + input.broadcastAuth = (session && (session as any).connectionHandle && (session as any).connectionHandle.broadcastAuth) === true; // ==== END OVERRIDE ==== - const self = this; - const reply = { + const self = this as any; + const reply: RateLimiterReply = { allowed: true, timeToReset: 0, numInvocationsLeft: Infinity, }; const matchedRules = self._findAllMatchingRules(input); - _.each(matchedRules, (rule) => { + _.each(matchedRules, (rule: any) => { // ==== BEGIN OVERRIDE ==== - const callbackReply = { + const callbackReply: RateLimiterReply = { allowed: true, timeToReset: 0, numInvocationsLeft: Infinity, @@ -118,33 +135,35 @@ RateLimiter.prototype.check = function (input) { return reply; }; -const checkNameNonStream = (name) => name && !names.has(name) && !name.startsWith('stream-'); -const checkNameForStream = (name) => name && !names.has(name) && name.startsWith('stream-'); - -const ruleIds = {}; - -const callback = (msg, name) => async (reply, input) => { - if (reply.allowed === false) { - rateLimiterLog({ msg, reply, input }); - metrics.ddpRateLimitExceeded.inc({ - limit_name: name, - user_id: input.userId, - client_address: input.clientAddress, - type: input.type, - name: input.name, - connection_id: input.connectionId, - }); - // sleep before sending the error to slow down next requests - if (slowDownRate > 0 && reply.numInvocationsExceeded) { - await sleep(slowDownRate * reply.numInvocationsExceeded); +const checkNameNonStream = (name: string): boolean => !!(name && !names.has(name) && !name.startsWith('stream-')); +const checkNameForStream = (name: string): boolean => !!(name && !names.has(name) && name.startsWith('stream-')); + +const ruleIds: Record = {}; + +const callback = + (msg: string, name: string) => + async (reply: RateLimiterReply, input: RateLimiterInput): Promise => { + if (reply.allowed === false) { + rateLimiterLog({ msg, reply, input }); + metrics.ddpRateLimitExceeded.inc({ + limit_name: name, + user_id: input.userId, + client_address: input.clientAddress, + type: input.type, + name: input.name, + connection_id: input.connectionId, + }); + // sleep before sending the error to slow down next requests + if (slowDownRate > 0 && reply.numInvocationsExceeded) { + await sleep(slowDownRate * reply.numInvocationsExceeded); + } + // } else { + // console.log('DDP RATE LIMIT:', message); + // console.log(JSON.stringify({ ...reply, ...input }, null, 2)); } - // } else { - // console.log('DDP RATE LIMIT:', message); - // console.log(JSON.stringify({ ...reply, ...input }, null, 2)); - } -}; + }; -const messages = { +const messages: Record = { IP: 'address', User: 'userId', Connection: 'connectionId', @@ -152,7 +171,7 @@ const messages = { Connection_By_Method: 'connectionId per method', }; -const reconfigureLimit = Meteor.bindEnvironment((name, rules, factor = 1) => { +const reconfigureLimit = Meteor.bindEnvironment((name: string, rules: any, factor = 1): void => { if (ruleIds[name + factor]) { DDPRateLimiter.removeRule(ruleIds[name + factor]); } @@ -161,68 +180,68 @@ const reconfigureLimit = Meteor.bindEnvironment((name, rules, factor = 1) => { return; } - ruleIds[name + factor] = addRule( + ruleIds[name + factor] = (addRule as any)( rules, - settings.get(`DDP_Rate_Limit_${name}_Requests_Allowed`) * factor, - settings.get(`DDP_Rate_Limit_${name}_Interval_Time`) * factor, + (settings.get(`DDP_Rate_Limit_${name}_Requests_Allowed`) as number) * factor, + (settings.get(`DDP_Rate_Limit_${name}_Interval_Time`) as number) * factor, callback(`limit by ${messages[name]}`, name), ); }); -const configIP = _.debounce(() => { +const configIP = _.debounce((): void => { reconfigureLimit('IP', { broadcastAuth: false, - clientAddress: (clientAddress) => clientAddress !== '127.0.0.1', + clientAddress: (clientAddress: string): boolean => clientAddress !== '127.0.0.1', }); }, 1000); -const configUser = _.debounce(() => { +const configUser = _.debounce((): void => { reconfigureLimit('User', { broadcastAuth: false, - userId: (userId) => userId != null, + userId: (userId: string | undefined): boolean => userId != null, }); }, 1000); -const configConnection = _.debounce(() => { +const configConnection = _.debounce((): void => { reconfigureLimit('Connection', { broadcastAuth: false, - connectionId: () => true, + connectionId: (): boolean => true, }); }, 1000); -const configUserByMethod = _.debounce(() => { +const configUserByMethod = _.debounce((): void => { reconfigureLimit('User_By_Method', { broadcastAuth: false, - type: () => true, + type: (): boolean => true, name: checkNameNonStream, - userId: (userId) => userId != null, + userId: (userId: string | undefined): boolean => userId != null, }); reconfigureLimit( 'User_By_Method', { broadcastAuth: false, - type: () => true, + type: (): boolean => true, name: checkNameForStream, - userId: (userId) => userId != null, + userId: (userId: string | undefined): boolean => userId != null, }, 4, ); }, 1000); -const configConnectionByMethod = _.debounce(() => { +const configConnectionByMethod = _.debounce((): void => { reconfigureLimit('Connection_By_Method', { broadcastAuth: false, - type: () => true, + type: (): boolean => true, name: checkNameNonStream, - connectionId: () => true, + connectionId: (): boolean => true, }); reconfigureLimit( 'Connection_By_Method', { broadcastAuth: false, - type: () => true, + type: (): boolean => true, name: checkNameForStream, - connectionId: () => true, + connectionId: (): boolean => true, }, 4, ); From f6e47eba2efc1def07bfe058ecf148e87d2c35b3 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 01:04:48 -0300 Subject: [PATCH 41/80] refactor: convert `apps/meteor/app/lib/server/startup/robots.js` to TypeScript --- apps/meteor/app/lib/server/startup/{robots.js => robots.ts} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename apps/meteor/app/lib/server/startup/{robots.js => robots.ts} (58%) diff --git a/apps/meteor/app/lib/server/startup/robots.js b/apps/meteor/app/lib/server/startup/robots.ts similarity index 58% rename from apps/meteor/app/lib/server/startup/robots.js rename to apps/meteor/app/lib/server/startup/robots.ts index 2225b2b725e08..6b7b441d73207 100644 --- a/apps/meteor/app/lib/server/startup/robots.js +++ b/apps/meteor/app/lib/server/startup/robots.ts @@ -1,10 +1,12 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import { settings } from '../../../settings/server'; Meteor.startup(() => { - return WebApp.connectHandlers.use('/robots.txt', (req, res /* , next*/) => { + return WebApp.connectHandlers.use('/robots.txt', (_req: IncomingMessage, res: ServerResponse /* , next*/): void => { res.writeHead(200); res.end(settings.get('Robot_Instructions_File_Content')); }); From 7e72cb9dbf729e632fb2688a4a66c445c3ef4a6e Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 08:07:33 -0300 Subject: [PATCH 42/80] refactor: convert `apps/meteor/app/lib/server/lib/validateEmailDomain.js` to TypeScript --- .../app/lib/server/functions/saveUser/saveNewUser.ts | 4 +++- .../{validateEmailDomain.js => validateEmailDomain.ts} | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) rename apps/meteor/app/lib/server/lib/{validateEmailDomain.js => validateEmailDomain.ts} (87%) diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts index 35fb39b5336f6..f00f59094c39f 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts @@ -13,7 +13,9 @@ import type { SaveUserData } from './saveUser'; import { sendPasswordEmail, sendWelcomeEmail } from './sendUserEmail'; export const saveNewUser = async function (userData: SaveUserData, sendPassword: boolean, performedBy: IUser) { - await validateEmailDomain(userData.email); + if (userData.email) { + await validateEmailDomain(userData.email); + } const roles = (!!userData.roles && userData.roles.length > 0 && userData.roles) || getNewUserRoles(); const isGuest = roles && roles.length === 1 && roles.includes('guest'); diff --git a/apps/meteor/app/lib/server/lib/validateEmailDomain.js b/apps/meteor/app/lib/server/lib/validateEmailDomain.ts similarity index 87% rename from apps/meteor/app/lib/server/lib/validateEmailDomain.js rename to apps/meteor/app/lib/server/lib/validateEmailDomain.ts index d00a8808e8aee..d34652be66134 100644 --- a/apps/meteor/app/lib/server/lib/validateEmailDomain.js +++ b/apps/meteor/app/lib/server/lib/validateEmailDomain.ts @@ -9,8 +9,8 @@ import { settings } from '../../../settings/server'; const dnsResolveMx = util.promisify(dns.resolveMx); -let emailDomainBlackList = []; -let emailDomainWhiteList = []; +let emailDomainBlackList: string[] = []; +let emailDomainWhiteList: string[] = []; settings.watch('Accounts_BlockedDomainsList', (value) => { if (!value) { @@ -18,7 +18,7 @@ settings.watch('Accounts_BlockedDomainsList', (value) => { return; } - emailDomainBlackList = value + emailDomainBlackList = (value as string) .split(',') .filter(Boolean) .map((domain) => domain.trim()); @@ -29,13 +29,13 @@ settings.watch('Accounts_AllowedDomainsList', (value) => { return; } - emailDomainWhiteList = value + emailDomainWhiteList = (value as string) .split(',') .filter(Boolean) .map((domain) => domain.trim()); }); -export const validateEmailDomain = async function (email) { +export const validateEmailDomain = async function (email: string): Promise { if (!validateEmail(email)) { throw new Meteor.Error('error-invalid-email', `Invalid email ${email}`, { function: 'RocketChat.validateEmailDomain', From dd212ef4bd012d89a1172ac07ff991bd9af69b17 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 10:00:10 -0300 Subject: [PATCH 43/80] refactor: convert `apps/meteor/app/lib/server/lib/debug.js` to TypeScript --- .../app/lib/server/lib/{debug.js => debug.ts} | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) rename apps/meteor/app/lib/server/lib/{debug.js => debug.ts} (58%) diff --git a/apps/meteor/app/lib/server/lib/debug.js b/apps/meteor/app/lib/server/lib/debug.ts similarity index 58% rename from apps/meteor/app/lib/server/lib/debug.js rename to apps/meteor/app/lib/server/lib/debug.ts index 71daf1a4fbcfc..3c5af9f29ec98 100644 --- a/apps/meteor/app/lib/server/lib/debug.js +++ b/apps/meteor/app/lib/server/lib/debug.ts @@ -1,3 +1,5 @@ +import type { IncomingMessage, ServerResponse } from 'http'; + import { InstanceStatus } from '@rocket.chat/instance-status'; import { Logger } from '@rocket.chat/logger'; import { tracerActiveSpan } from '@rocket.chat/tracing'; @@ -12,25 +14,32 @@ import { getModifiedHttpHeaders } from '../functions/getModifiedHttpHeaders'; const logger = new Logger('Meteor'); -let Log_Trace_Methods; -let Log_Trace_Subscriptions; +let logTraceMethods: boolean | undefined; +let logTraceSubscriptions: boolean | undefined; settings.watch('Log_Trace_Methods', (value) => { - Log_Trace_Methods = value; + logTraceMethods = value as boolean; }); settings.watch('Log_Trace_Subscriptions', (value) => { - Log_Trace_Subscriptions = value; + logTraceSubscriptions = value as boolean; }); -let Log_Trace_Methods_Filter; -let Log_Trace_Subscriptions_Filter; +let logTraceMethodsFilter: RegExp | undefined; +let logTraceSubscriptionsFilter: RegExp | undefined; settings.watch('Log_Trace_Methods_Filter', (value) => { - Log_Trace_Methods_Filter = value ? new RegExp(value) : undefined; + logTraceMethodsFilter = value ? new RegExp(value as string) : undefined; }); settings.watch('Log_Trace_Subscriptions_Filter', (value) => { - Log_Trace_Subscriptions_Filter = value ? new RegExp(value) : undefined; + logTraceSubscriptionsFilter = value ? new RegExp(value as string) : undefined; }); -const traceConnection = (enable, filter, prefix, name, connection, userId) => { +const traceConnection = ( + enable: boolean | undefined, + filter: RegExp | undefined, + _prefix: string, + name: string, + connection: Meteor.Connection | null | undefined, + userId: string | null, +): void => { if (!enable) { return; } @@ -51,13 +60,13 @@ const traceConnection = (enable, filter, prefix, name, connection, userId) => { } }; -const wrapMethods = function (name, originalHandler, methodsMap) { - methodsMap[name] = function (...originalArgs) { - traceConnection(Log_Trace_Methods, Log_Trace_Methods_Filter, 'method', name, this.connection, this.userId); +const wrapMethods = function (name: string, originalHandler: (...args: any[]) => any, methodsMap: Record): void { + methodsMap[name] = function (this: Meteor.MethodThisType, ...originalArgs: any[]) { + traceConnection(logTraceMethods, logTraceMethodsFilter, 'method', name, this.connection, this.userId); const method = name === 'stream' ? `${name}:${originalArgs[0]}` : name; - const end = metrics.meteorMethods.startTimer({ + const end = (metrics.meteorMethods.startTimer as any)({ method, has_connection: this.connection != null, has_user: this.userId != null, @@ -78,7 +87,7 @@ const wrapMethods = function (name, originalHandler, methodsMap) { { attributes: { method: name, - userId: this.userId, + userId: this.userId ?? undefined, }, }, async () => { @@ -92,7 +101,7 @@ const wrapMethods = function (name, originalHandler, methodsMap) { const originalMeteorMethods = Meteor.methods; -Meteor.methods = function (methodMap) { +(Meteor as any).methods = function (methodMap: Record any>): void { _.each(methodMap, (handler, name) => { wrapMethods(name, handler, methodMap); }); @@ -101,9 +110,9 @@ Meteor.methods = function (methodMap) { const originalMeteorPublish = Meteor.publish; -Meteor.publish = function (name, func) { - return originalMeteorPublish(name, function (...args) { - traceConnection(Log_Trace_Subscriptions, Log_Trace_Subscriptions_Filter, 'subscription', name, this.connection, this.userId); +(Meteor as any).publish = function (name: string, func: (...args: any[]) => any) { + return originalMeteorPublish(name, function (this: any, ...args: any[]) { + traceConnection(logTraceSubscriptions, logTraceSubscriptionsFilter, 'subscription', name, this.connection, this.userId); logger.subscription({ publication: name, @@ -114,19 +123,19 @@ Meteor.publish = function (name, func) { instanceId: InstanceStatus.id(), }); - const end = metrics.meteorSubscriptions.startTimer({ subscription: name }); + const end = (metrics.meteorSubscriptions.startTimer as any)({ subscription: name }); const originalReady = this.ready; - this.ready = function () { + this.ready = function (this: any) { end(); - return originalReady.apply(this, args); + return originalReady.call(this); }; return func.apply(this, args); }); }; -WebApp.rawConnectHandlers.use((req, res, next) => { +WebApp.rawConnectHandlers.use((_req: IncomingMessage, res: ServerResponse, next: () => void) => { res.setHeader('X-Instance-ID', InstanceStatus.id()); return next(); }); From e5bbf5ae1990b010068ab337e932ddd4451fc170 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 15:19:08 -0300 Subject: [PATCH 44/80] refactor: convert `apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js` to TypeScript --- ...mails.js => interceptDirectReplyEmails.ts} | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) rename apps/meteor/app/lib/server/lib/{interceptDirectReplyEmails.js => interceptDirectReplyEmails.ts} (74%) diff --git a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.ts similarity index 74% rename from apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js rename to apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.ts index 77aaa03887c54..63e1122f6cd92 100644 --- a/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.js +++ b/apps/meteor/app/lib/server/lib/interceptDirectReplyEmails.ts @@ -1,3 +1,4 @@ +// @ts-expect-error - no types available for @rocket.chat/poplib import POP3Lib from '@rocket.chat/poplib'; import { simpleParser } from 'mailparser'; @@ -6,7 +7,7 @@ import { IMAPInterceptor } from '../../../../server/email/IMAPInterceptor'; import { settings } from '../../../settings/server'; export class DirectReplyIMAPInterceptor extends IMAPInterceptor { - constructor(imapConfig, options = {}) { + constructor(imapConfig: Record = {}, options: Record = {}) { imapConfig = { user: settings.get('Direct_Reply_Username'), password: settings.get('Direct_Reply_Password'), @@ -19,13 +20,19 @@ export class DirectReplyIMAPInterceptor extends IMAPInterceptor { options.deleteAfterRead = settings.get('Direct_Reply_Delete'); - super(imapConfig, options); + super(imapConfig as any, options as any, 'direct-reply'); this.on('email', (email) => processDirectEmail(email)); } } class POP3Intercepter { + pop3: any; + + totalMsgCount: number; + + currentMsgCount: number; + constructor() { this.pop3 = new POP3Lib(settings.get('Direct_Reply_Port'), settings.get('Direct_Reply_Host'), { enabletls: !settings.get('Direct_Reply_IgnoreTLS'), @@ -41,7 +48,7 @@ class POP3Intercepter { this.pop3.login(settings.get('Direct_Reply_Username'), settings.get('Direct_Reply_Password')); }); - this.pop3.on('login', (status) => { + this.pop3.on('login', (status: boolean) => { if (!status) { return console.log('Unable to Log-in ....'); } @@ -51,7 +58,7 @@ class POP3Intercepter { }); // on getting list of all emails - this.pop3.on('list', (status, msgcount) => { + this.pop3.on('list', (status: boolean, msgcount: number) => { if (!status) { console.log('Cannot Get Emails ....'); } @@ -66,14 +73,14 @@ class POP3Intercepter { }); // on retrieved email - this.pop3.on('retr', async (status, msgnumber, data) => { + this.pop3.on('retr', (status: boolean, msgnumber: number, data: any) => { if (!status) { return console.log('Cannot Retrieve Message ....'); } // parse raw email data to JSON object - simpleParser(data, (err, mail) => { - processDirectEmail(mail); + simpleParser(data, (_err, mail) => { + void processDirectEmail(mail); }); this.currentMsgCount += 1; @@ -83,7 +90,7 @@ class POP3Intercepter { }); // on email deleted - this.pop3.on('dele', (status) => { + this.pop3.on('dele', (status: boolean) => { if (!status) { return console.log('Cannot Delete Message....'); } @@ -98,34 +105,40 @@ class POP3Intercepter { }); // invalid server state - this.pop3.on('invalid-state', (cmd) => { + this.pop3.on('invalid-state', (cmd: string) => { console.log(`Invalid state. You tried calling ${cmd}`); }); - this.pop3.on('error', (cmd) => { + this.pop3.on('error', (cmd: string) => { console.log(`error state. You tried calling ${cmd}`); }); // locked => command already running, not finished yet - this.pop3.on('locked', (cmd) => { + this.pop3.on('locked', (cmd: string) => { console.log(`Current command has not finished yet. You tried calling ${cmd}`); }); } } export class POP3Helper { - constructor(frequency) { + frequency: number; + + running: NodeJS.Timeout | false; + + POP3: POP3Intercepter; + + constructor(frequency: number) { this.frequency = frequency; this.running = false; this.POP3 = new POP3Intercepter(); } - isActive() { - return this.running; + isActive(): boolean { + return this.running !== false; } - start() { + start(): void { this.log('POP3 started'); this.running = setInterval( () => { @@ -136,16 +149,18 @@ export class POP3Helper { ); } - log(...args) { + log(...args: any[]): void { console.log(...args); } - stop(callback = new Function()) { + stop(callback: () => void = undefined as any): void { this.log('POP3 stop called'); if (this.isActive()) { - clearInterval(this.running); + clearInterval(this.running as NodeJS.Timeout); + } + if (callback) { + callback(); } - callback(); this.log('POP3 stopped'); } } From 8548153f84b2a843e3762cd979defb01b1a2e974 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 15:51:10 -0300 Subject: [PATCH 45/80] refactor: convert `apps/meteor/app/lib/server/lib/RateLimiter.js` to TypeScript --- .../lib/{RateLimiter.js => RateLimiter.ts} | 13 ++++++++---- .../configuration/configureDirectReply.ts | 2 +- apps/meteor/server/methods/registerUser.ts | 20 ++++++++++++++----- 3 files changed, 25 insertions(+), 10 deletions(-) rename apps/meteor/app/lib/server/lib/{RateLimiter.js => RateLimiter.ts} (53%) diff --git a/apps/meteor/app/lib/server/lib/RateLimiter.js b/apps/meteor/app/lib/server/lib/RateLimiter.ts similarity index 53% rename from apps/meteor/app/lib/server/lib/RateLimiter.js rename to apps/meteor/app/lib/server/lib/RateLimiter.ts index cdb32299263d2..0014d0bfb1bb1 100644 --- a/apps/meteor/app/lib/server/lib/RateLimiter.js +++ b/apps/meteor/app/lib/server/lib/RateLimiter.ts @@ -1,16 +1,21 @@ import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; export const RateLimiterClass = new (class { - limitMethod(methodName, numRequests, timeInterval, matchers) { + limitMethod( + methodName: string, + numRequests: number, + timeInterval: number, + matchers: Record boolean | Promise>, + ): string | null { if (process.env.TEST_MODE === 'true') { - return; + return null; } - const match = { + const match: Record = { type: 'method', name: methodName, }; Object.entries(matchers).forEach(([key, matcher]) => { - match[key] = (...args) => matcher(...args); + match[key] = (...args: any[]) => matcher(...args); }); return DDPRateLimiter.addRule(match, numRequests, timeInterval); } diff --git a/apps/meteor/server/configuration/configureDirectReply.ts b/apps/meteor/server/configuration/configureDirectReply.ts index e7266d10df7eb..5bf44276aa8ec 100644 --- a/apps/meteor/server/configuration/configureDirectReply.ts +++ b/apps/meteor/server/configuration/configureDirectReply.ts @@ -1,6 +1,6 @@ import _ from 'underscore'; -import { DirectReplyIMAPInterceptor, POP3Helper } from '../../app/lib/server/lib/interceptDirectReplyEmails.js'; +import { DirectReplyIMAPInterceptor, POP3Helper } from '../../app/lib/server/lib/interceptDirectReplyEmails'; import type { ICachedSettings } from '../../app/settings/server/CachedSettings'; import { logger } from '../features/EmailInbox/logger'; diff --git a/apps/meteor/server/methods/registerUser.ts b/apps/meteor/server/methods/registerUser.ts index a7b6e6e4506f5..d2c30512f90e9 100644 --- a/apps/meteor/server/methods/registerUser.ts +++ b/apps/meteor/server/methods/registerUser.ts @@ -150,7 +150,7 @@ let registerUserRuleId = RateLimiter.limitMethod( }, ); -settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { +settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { // When running on testMode, there's no rate limiting added, so this function throws an error if (process.env.TEST_MODE === 'true') { return; @@ -159,11 +159,21 @@ settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { if (!registerUserRuleId) { throw new Error('Rate limiter rule for "registerUser" not found'); } + + if (typeof value !== 'number') { + return; + } + // remove old DDP rate limiter rule and create a new one with the updated setting value DDPRateLimiter.removeRule(registerUserRuleId); - registerUserRuleId = RateLimiter.limitMethod('registerUser', value, settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), { - userId() { - return true; + registerUserRuleId = RateLimiter.limitMethod( + 'registerUser', + value, + (settings.get('API_Enable_Rate_Limiter_Limit_Time_Default') as number) ?? 60000, + { + userId() { + return true; + }, }, - }); + ); }); From c54b744455d0b19a5eca5922cb0e09ebaa00719f Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 16:13:22 -0300 Subject: [PATCH 46/80] refactor: convert `apps/meteor/app/lib/server/functions/validateCustomFields.js` to TypeScript --- ...CustomFields.js => validateCustomFields.ts} | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) rename apps/meteor/app/lib/server/functions/{validateCustomFields.js => validateCustomFields.ts} (78%) diff --git a/apps/meteor/app/lib/server/functions/validateCustomFields.js b/apps/meteor/app/lib/server/functions/validateCustomFields.ts similarity index 78% rename from apps/meteor/app/lib/server/functions/validateCustomFields.js rename to apps/meteor/app/lib/server/functions/validateCustomFields.ts index de420a9a4c795..d623fff61ad48 100644 --- a/apps/meteor/app/lib/server/functions/validateCustomFields.js +++ b/apps/meteor/app/lib/server/functions/validateCustomFields.ts @@ -3,21 +3,29 @@ import { Meteor } from 'meteor/meteor'; import { trim } from '../../../../lib/utils/stringUtils'; import { settings } from '../../../settings/server'; -export const validateCustomFields = function (fields) { +type CustomFieldMeta = { + required?: boolean; + type?: string; + options?: string[]; + maxLength?: number; + minLength?: number; +}; + +export const validateCustomFields = function (fields: Record): void { // Special Case: // If an admin didn't set any custom fields there's nothing to validate against so consider any customFields valid if (trim(settings.get('Accounts_CustomFields')) === '') { return; } - let customFieldsMeta; + let customFieldsMeta: Record; try { - customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields')); + customFieldsMeta = JSON.parse(settings.get('Accounts_CustomFields') as string); } catch (e) { throw new Meteor.Error('error-invalid-customfield-json', 'Invalid JSON for Custom Fields'); } - const customFields = {}; + const customFields: Record = {}; Object.keys(customFieldsMeta).forEach((fieldName) => { const field = customFieldsMeta[fieldName]; @@ -29,7 +37,7 @@ export const validateCustomFields = function (fields) { throw new Meteor.Error('error-user-registration-custom-field', `Field ${fieldName} is required`, { method: 'registerUser' }); } - if (field.type === 'select' && field.options.indexOf(fields[fieldName]) === -1) { + if (field.type === 'select' && field.options?.indexOf(fields[fieldName]) === -1) { throw new Meteor.Error('error-user-registration-custom-field', `Value for field ${fieldName} is invalid`, { method: 'registerUser' }); } From a46a8f6e475923ec99f4923314e0455d2def79d2 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 16:20:05 -0300 Subject: [PATCH 47/80] refactor: convert `apps/meteor/app/metrics/server/lib/statsTracker.js` to TypeScript --- .../app/metrics/server/lib/statsTracker.js | 47 ----------------- .../app/metrics/server/lib/statsTracker.ts | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 47 deletions(-) delete mode 100644 apps/meteor/app/metrics/server/lib/statsTracker.js create mode 100644 apps/meteor/app/metrics/server/lib/statsTracker.ts diff --git a/apps/meteor/app/metrics/server/lib/statsTracker.js b/apps/meteor/app/metrics/server/lib/statsTracker.js deleted file mode 100644 index d46d7597c20e0..0000000000000 --- a/apps/meteor/app/metrics/server/lib/statsTracker.js +++ /dev/null @@ -1,47 +0,0 @@ -import { StatsD } from 'node-dogstatsd'; - -class StatsTracker { - constructor() { - this.StatsD = StatsD; - this.dogstatsd = new this.StatsD(); - } - - track(type, stats, ...args) { - this.dogstatsd[type](`RocketChat.${stats}`, ...args); - } - - now() { - const hrtime = process.hrtime(); - return hrtime[0] * 1000000 + hrtime[1] / 1000; - } - - timing(stats, time, tags) { - this.track('timing', stats, time, tags); - } - - increment(stats, time, tags) { - this.track('increment', stats, time, tags); - } - - decrement(stats, time, tags) { - this.track('decrement', stats, time, tags); - } - - histogram(stats, time, tags) { - this.track('histogram', stats, time, tags); - } - - gauge(stats, time, tags) { - this.track('gauge', stats, time, tags); - } - - unique(stats, time, tags) { - this.track('unique', stats, time, tags); - } - - set(stats, time, tags) { - this.track('set', stats, time, tags); - } -} - -export default new StatsTracker(); diff --git a/apps/meteor/app/metrics/server/lib/statsTracker.ts b/apps/meteor/app/metrics/server/lib/statsTracker.ts new file mode 100644 index 0000000000000..ddd94e808c243 --- /dev/null +++ b/apps/meteor/app/metrics/server/lib/statsTracker.ts @@ -0,0 +1,52 @@ +// @ts-expect-error - no types available for node-dogstatsd +import { StatsD } from 'node-dogstatsd'; + +class StatsTracker { + StatsD: typeof StatsD; + + dogstatsd: StatsD; + + constructor() { + this.StatsD = StatsD; + this.dogstatsd = new this.StatsD(); + } + + track(type: string, stats: string, ...args: any[]): void { + (this.dogstatsd as any)[type](`RocketChat.${stats}`, ...args); + } + + now(): number { + const hrtime = process.hrtime(); + return hrtime[0] * 1000000 + hrtime[1] / 1000; + } + + timing(stats: string, time: number, tags?: string[]): void { + this.track('timing', stats, time, tags); + } + + increment(stats: string, time?: number, tags?: string[]): void { + this.track('increment', stats, time, tags); + } + + decrement(stats: string, time?: number, tags?: string[]): void { + this.track('decrement', stats, time, tags); + } + + histogram(stats: string, time: number, tags?: string[]): void { + this.track('histogram', stats, time, tags); + } + + gauge(stats: string, time: number, tags?: string[]): void { + this.track('gauge', stats, time, tags); + } + + unique(stats: string, time: any, tags?: string[]): void { + this.track('unique', stats, time, tags); + } + + set(stats: string, time: any, tags?: string[]): void { + this.track('set', stats, time, tags); + } +} + +export default new StatsTracker(); From ccbdf3f553f3812bf32910f3126503d42d7bd55f Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 16:26:15 -0300 Subject: [PATCH 48/80] refactor: convert `apps/meteor/app/statistics/server/lib/UAParserCustom.js` to TypeScript --- .../{UAParserCustom.js => UAParserCustom.ts} | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) rename apps/meteor/app/statistics/server/lib/{UAParserCustom.js => UAParserCustom.ts} (82%) diff --git a/apps/meteor/app/statistics/server/lib/UAParserCustom.js b/apps/meteor/app/statistics/server/lib/UAParserCustom.ts similarity index 82% rename from apps/meteor/app/statistics/server/lib/UAParserCustom.js rename to apps/meteor/app/statistics/server/lib/UAParserCustom.ts index 679c039cf9089..ef31a566add36 100644 --- a/apps/meteor/app/statistics/server/lib/UAParserCustom.js +++ b/apps/meteor/app/statistics/server/lib/UAParserCustom.ts @@ -1,6 +1,6 @@ import UAParser from 'ua-parser-js'; -const mergeDeep = (target, source) => { +const mergeDeep = (target: any, source: any): any => { if (!(typeof target === 'object' && typeof source === 'object')) { return target; } @@ -26,6 +26,11 @@ const mergeDeep = (target, source) => { return target; }; +type PropConfig = { + list: string[]; + get?: (prop: string, value: string) => string; +}; + export const UAParserMobile = { appName: 'RC Mobile', device: 'mobile-app', @@ -36,7 +41,7 @@ export const UAParserMobile = { }, app: { list: ['version', 'bundle'], - get: (prop, value) => { + get: (prop: string, value: string) => { if (prop === 'bundle') { return value.replace(/([()])/g, ''); } @@ -48,15 +53,15 @@ export const UAParserMobile = { return value; }, }, - }, + } as Record, - isMobileApp(uaString) { + isMobileApp(uaString: string): boolean { if (!uaString || typeof uaString !== 'string') { return false; } const splitUA = uaString.split(this.uaSeparator); - return splitUA && splitUA[0] && splitUA[0].trim() === this.appName; + return !!(splitUA && splitUA[0] && splitUA[0].trim() === this.appName); }, /** @@ -64,14 +69,14 @@ export const UAParserMobile = { * @param {string} uaString * @returns { device: { type: '' }, app: { name: '', version: '' } } */ - uaObject(uaString) { + uaObject(uaString: string): Record { if (!this.isMobileApp(uaString)) { return {}; } const splitUA = uaString.split(this.uaSeparator); - let obj = { + let obj: any = { device: { type: this.device, }, @@ -97,7 +102,7 @@ export const UAParserMobile = { return; } - const subProps = {}; + const subProps: Record = {}; splitProps.forEach((value, idx) => { if (props.list.length > idx) { const propName = props.list[idx]; @@ -105,7 +110,7 @@ export const UAParserMobile = { } }); - const prop = {}; + const prop: Record = {}; prop[key] = subProps; obj = mergeDeep(obj, prop); }); @@ -117,7 +122,7 @@ export const UAParserMobile = { export const UAParserDesktop = { device: 'desktop-app', - isDesktopApp(uaString) { + isDesktopApp(uaString: string): boolean { if (!uaString || typeof uaString !== 'string') { return false; } @@ -130,7 +135,7 @@ export const UAParserDesktop = { * @param {string} uaString * @returns { device: { type: '' }, os: '' || {}, app: { name: '', version: '' } } */ - uaObject(uaString) { + uaObject(uaString: string): Record { if (!this.isDesktopApp(uaString)) { return {}; } From cc2950b044a6ba7cc3ea427940a525294068a147 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 16:56:29 -0300 Subject: [PATCH 49/80] refactor: convert `apps/meteor/app/token-login/server/login_token_server.js` to TypeScript --- .../server/{login_token_server.js => login_token_server.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/meteor/app/token-login/server/{login_token_server.js => login_token_server.ts} (100%) diff --git a/apps/meteor/app/token-login/server/login_token_server.js b/apps/meteor/app/token-login/server/login_token_server.ts similarity index 100% rename from apps/meteor/app/token-login/server/login_token_server.js rename to apps/meteor/app/token-login/server/login_token_server.ts From a73dd414d83cc1b1f9f18b554279c06af35445d0 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 17:06:05 -0300 Subject: [PATCH 50/80] refactor: convert `apps/meteor/app/lib/server/functions/notifications/mobile.js` to TypeScript --- .../notifications/{mobile.js => mobile.ts} | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) rename apps/meteor/app/lib/server/functions/notifications/{mobile.js => mobile.ts} (75%) diff --git a/apps/meteor/app/lib/server/functions/notifications/mobile.js b/apps/meteor/app/lib/server/functions/notifications/mobile.ts similarity index 75% rename from apps/meteor/app/lib/server/functions/notifications/mobile.js rename to apps/meteor/app/lib/server/functions/notifications/mobile.ts index 1ce3167966082..8481f9e30a023 100644 --- a/apps/meteor/app/lib/server/functions/notifications/mobile.js +++ b/apps/meteor/app/lib/server/functions/notifications/mobile.ts @@ -1,3 +1,4 @@ +import type { AtLeast, IMessage, IRoom, IUser, RoomType } from '@rocket.chat/core-typings'; import { Subscriptions } from '@rocket.chat/models'; import { i18n } from '../../../../../server/lib/i18n'; @@ -8,7 +9,10 @@ import { settings } from '../../../../settings/server'; const CATEGORY_MESSAGE = 'MESSAGE'; const CATEGORY_MESSAGE_NOREPLY = 'MESSAGE_NOREPLY'; -function enableNotificationReplyButton(room, username) { +function enableNotificationReplyButton(room: IRoom, username: string | undefined): boolean { + if (!username) { + return false; + } // Some users may have permission to send messages even on readonly rooms, but we're ok with false negatives here in exchange of better perfomance if (room.ro === true) { return false; @@ -30,12 +34,21 @@ export async function getPushData({ notificationMessage, receiver, shouldOmitMessage = true, +}: { + room: IRoom; + message: AtLeast; + userId: string; + senderUsername: string | undefined; + senderName: string | undefined; + notificationMessage: string; + receiver: AtLeast; + shouldOmitMessage?: boolean; }) { const username = settings.get('Push_show_username_room') ? (settings.get('UI_Use_Real_Name') && senderName) || senderUsername : ''; - const lng = receiver.language || settings.get('Language') || 'en'; + const lng = receiver.language || settings.get('Language') || 'en'; - let messageText; + let messageText: string; if (shouldOmitMessage && settings.get('Push_request_content_from_server')) { messageText = i18n.t('You_have_a_new_message', { lng }); } else if (!settings.get('Push_show_message')) { @@ -47,7 +60,7 @@ export async function getPushData({ return { payload: { sender: message.u, - senderName: username, + senderName: username || '', type: room.t, name: settings.get('Push_show_username_room') ? room.name : '', messageType: message.t, @@ -58,7 +71,7 @@ export async function getPushData({ settings.get('Push_show_username_room') && roomCoordinator.getRoomDirectives(room.t).isGroupChat(room) ? `#${await roomCoordinator.getRoomName(room.t, room, userId)}` : '', - username, + username: username || '', message: messageText, badge: await Subscriptions.getBadgeCount(userId), category: enableNotificationReplyButton(room, receiver.username) ? CATEGORY_MESSAGE : CATEGORY_MESSAGE_NOREPLY, @@ -77,7 +90,19 @@ export function shouldNotifyMobile({ isVideoConf, userPreferences, roomUids, -}) { +}: { + disableAllMessageNotifications: boolean; + mobilePushNotifications: string | null | undefined; + hasMentionToAll: boolean; + isHighlighted: boolean; + hasMentionToUser: boolean; + hasReplyToThread: boolean; + roomType: RoomType; + isThread: boolean; + isVideoConf: boolean; + userPreferences: { enableMobileRinging?: boolean } | undefined; + roomUids: string[] | undefined; +}): boolean { if (settings.get('Push_enable') !== true) { return false; } From d0125f7603c01340fb01de6de8147bf7a9a21002 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 17:18:50 -0300 Subject: [PATCH 51/80] refactor: convert `apps/meteor/app/lib/server/functions/notifications/email.js` to TypeScript --- .../notifications/{email.js => email.ts} | 115 ++++++++++++++---- 1 file changed, 93 insertions(+), 22 deletions(-) rename apps/meteor/app/lib/server/functions/notifications/{email.js => email.ts} (67%) diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.ts similarity index 67% rename from apps/meteor/app/lib/server/functions/notifications/email.js rename to apps/meteor/app/lib/server/functions/notifications/email.ts index a27699bc1d111..e812553114e48 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.ts @@ -1,3 +1,4 @@ +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -16,13 +17,30 @@ Meteor.startup(() => { settings.watch('email_style', () => { goToMessage = Mailer.inlinecss('

{Offline_Link_Message}

'); }); - Mailer.getTemplate('Email_Footer_Direct_Reply', (value) => { + Mailer.getTemplate('Email_Footer_Direct_Reply', (value: string) => { advice = value; }); }); -export async function getEmailContent({ message, user, room }) { - const lng = (user && user.language) || settings.get('Language') || 'en'; +export async function getEmailContent({ + message, + user, + room, +}: { + message: Partial & { + _id: string; + u: Required> & Pick; + msg: string; + t?: string; + tokens?: any[]; + file?: any; + files?: any[]; + attachments?: any[]; + }; + user: (Partial & { language?: string; name?: string; username?: string }) | null; + room: IRoom; +}): Promise { + const lng = (user && user.language) || settings.get('Language') || 'en'; const roomName = escapeHTML(`#${await roomCoordinator.getRoomName(room.t, room)}`); const userName = escapeHTML(settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username); @@ -34,11 +52,11 @@ export async function getEmailContent({ message, user, room }) { const hasText = typeof message.msg === 'string' && message.msg.trim() !== ''; const isGroupChat = roomDirectives.isGroupChat(room); - let header; + let header: string; if (hasFiles && !hasText) { // file-only message const isMultipleFiles = files.length > 1; - let headerKey; + let headerKey: string; if (isGroupChat) { headerKey = isMultipleFiles ? 'User_uploaded_files_on_channel' : 'User_uploaded_a_file_on_channel'; } else { @@ -62,7 +80,7 @@ export async function getEmailContent({ message, user, room }) { return header; } - const contentParts = []; + const contentParts: string[] = []; if (hasText) { let messageContent = escapeHTML(message.msg); @@ -79,6 +97,9 @@ export async function getEmailContent({ message, user, room }) { if (hasFiles) { const attachments = message.attachments || []; const fileParts = files.map((file, index) => { + if (!file) { + return ''; + } let part = escapeHTML(file.name); if (attachments[index]?.description) { part += `

${escapeHTML(attachments[index].description)}`; @@ -110,8 +131,18 @@ export async function getEmailContent({ message, user, room }) { return header; } -const getButtonUrl = (room, subscription, message) => { - const basePath = roomCoordinator.getRouteLink(room.t, subscription).replace(Meteor.absoluteUrl(), ''); +const getButtonUrl = ( + room: IRoom, + subscription: any, + message: Partial & { + _id: string; + u: Required> & Pick; + msg: string; + tmid?: string; + }, +): string => { + const routeLink = roomCoordinator.getRouteLink(room.t, subscription); + const basePath = typeof routeLink === 'string' ? routeLink.replace(Meteor.absoluteUrl(), '') : ''; const path = `${ltrim(basePath, '/')}?msg=${message._id}`; return getURL( @@ -129,11 +160,32 @@ const getButtonUrl = (room, subscription, message) => { ); }; -function generateNameEmail(name, email) { +function generateNameEmail(name: string, email: string): string { return `${String(name).replace(/@/g, '%40').replace(/[<>,]/g, '')} <${email}>`; } -export async function getEmailData({ message, receiver, sender, subscription, room, emailAddress, hasMentionToUser }) { +export async function getEmailData({ + message, + receiver, + sender, + subscription, + room, + emailAddress, + hasMentionToUser, +}: { + message: Partial & { + _id: string; + u: Required> & Pick; + msg: string; + tmid?: string; + }; + receiver: Partial & { language?: string; name?: string; username?: string }; + sender: Partial & { _id: string; name?: string; username?: string; emails?: { address: string }[] }; + subscription: any; + room: IRoom; + emailAddress: string; + hasMentionToUser: boolean; +}) { const username = settings.get('UI_Use_Real_Name') ? message.u.name || message.u.username : message.u.username; let subjectKey = 'Offline_Mention_All_Email'; @@ -153,41 +205,50 @@ export async function getEmailData({ message, receiver, sender, subscription, ro room, }); - const room_path = getButtonUrl(room, subscription, message); + const roomPath = getButtonUrl(room, subscription, message); const receiverName = settings.get('UI_Use_Real_Name') ? receiver.name || receiver.username : receiver.username; - const email = { + const email: { + from: string; + to: string; + subject: string; + html: string; + data: { room_path: string }; + headers: Record; + } = { from: generateNameEmail(username, settings.get('From_Email')), - to: generateNameEmail(receiverName, emailAddress), + to: generateNameEmail(receiverName || '', emailAddress), subject: emailSubject, html: content + goToMessage + (settings.get('Direct_Reply_Enable') ? advice : ''), data: { - room_path, + room_path: roomPath, }, headers: {}, }; - if (sender.emails?.length > 0 && settings.get('Add_Sender_To_ReplyTo')) { + if (sender.emails?.length && sender.emails.length > 0 && settings.get('Add_Sender_To_ReplyTo')) { const [senderEmail] = sender.emails; email.headers['Reply-To'] = generateNameEmail(username, senderEmail.address); } // If direct reply enabled, email content with headers if (settings.get('Direct_Reply_Enable')) { - const replyto = settings.get('Direct_Reply_ReplyTo') || settings.get('Direct_Reply_Username'); + const replyto = (settings.get('Direct_Reply_ReplyTo') || settings.get('Direct_Reply_Username')) as string | undefined; - // Reply-To header with format "username+messageId@domain" - email.headers['Reply-To'] = `${replyto.split('@')[0].split(settings.get('Direct_Reply_Separator'))[0]}${settings.get( - 'Direct_Reply_Separator', - )}${message.tmid || message._id}@${replyto.split('@')[1]}`; + if (replyto && typeof replyto === 'string') { + // Reply-To header with format "username+messageId@domain" + email.headers['Reply-To'] = `${replyto.split('@')[0].split(settings.get('Direct_Reply_Separator') as string)[0]}${settings.get( + 'Direct_Reply_Separator', + )}${message.tmid || message._id}@${replyto.split('@')[1]}`; + } } metrics.notificationsSent.inc({ notification_type: 'email' }); return email; } -export function sendEmailFromData(data) { +export function sendEmailFromData(data: any) { metrics.notificationsSent.inc({ notification_type: 'email' }); return Mailer.send(data); } @@ -202,7 +263,17 @@ export function shouldNotifyEmail({ hasReplyToThread, roomType, isThread, -}) { +}: { + disableAllMessageNotifications: boolean; + statusConnection: string | undefined; + emailNotifications: string | null | undefined; + isHighlighted: boolean; + hasMentionToUser: boolean; + hasMentionToAll: boolean; + hasReplyToThread: boolean; + roomType: string; + isThread: boolean; +}): boolean { // email notifications are disabled globally if (!settings.get('Accounts_AllowEmailNotifications')) { return false; From bf271f353c5650d7ae88d12ce97aba84ff895127 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 17:22:19 -0300 Subject: [PATCH 52/80] refactor: convert `apps/meteor/app/lib/server/oauth/proxy.js` to TypeScript --- apps/meteor/app/lib/server/oauth/proxy.js | 12 ------------ apps/meteor/app/lib/server/oauth/proxy.ts | 13 +++++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) delete mode 100644 apps/meteor/app/lib/server/oauth/proxy.js create mode 100644 apps/meteor/app/lib/server/oauth/proxy.ts diff --git a/apps/meteor/app/lib/server/oauth/proxy.js b/apps/meteor/app/lib/server/oauth/proxy.js deleted file mode 100644 index c66143d8b029d..0000000000000 --- a/apps/meteor/app/lib/server/oauth/proxy.js +++ /dev/null @@ -1,12 +0,0 @@ -import { OAuth } from 'meteor/oauth'; -import _ from 'underscore'; - -import { settings } from '../../../settings/server'; - -OAuth._redirectUri = _.wrap(OAuth._redirectUri, (func, serviceName, ...args) => { - const proxy = settings.get('Accounts_OAuth_Proxy_services').replace(/\s/g, '').split(','); - if (proxy.includes(serviceName)) { - return `${settings.get('Accounts_OAuth_Proxy_host')}/oauth_redirect`; - } - return func(serviceName, ...args); -}); diff --git a/apps/meteor/app/lib/server/oauth/proxy.ts b/apps/meteor/app/lib/server/oauth/proxy.ts new file mode 100644 index 0000000000000..b30281c5b14f3 --- /dev/null +++ b/apps/meteor/app/lib/server/oauth/proxy.ts @@ -0,0 +1,13 @@ +import { OAuth } from 'meteor/oauth'; + +import { settings } from '../../../settings/server'; + +OAuth._redirectUri = ((func: (serviceName: string, ...args: any[]) => string) => { + return (serviceName: string, ...args: any[]): string => { + const proxy = settings.get('Accounts_OAuth_Proxy_services').replace(/\s/g, '').split(','); + if (proxy.includes(serviceName)) { + return `${settings.get('Accounts_OAuth_Proxy_host')}/oauth_redirect`; + } + return func(serviceName, ...args); + }; +})(OAuth._redirectUri); From a46d0b7800d135239130feaacffd87f2b22c9d61 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 17:31:39 -0300 Subject: [PATCH 53/80] refactor: convert `apps/meteor/app/lib/server/oauth/oauth.js` to TypeScript --- apps/meteor/app/lib/server/oauth/oauth.js | 55 ------------------- apps/meteor/app/lib/server/oauth/oauth.ts | 65 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 55 deletions(-) delete mode 100644 apps/meteor/app/lib/server/oauth/oauth.js create mode 100644 apps/meteor/app/lib/server/oauth/oauth.ts diff --git a/apps/meteor/app/lib/server/oauth/oauth.js b/apps/meteor/app/lib/server/oauth/oauth.js deleted file mode 100644 index 27342416bedbd..0000000000000 --- a/apps/meteor/app/lib/server/oauth/oauth.js +++ /dev/null @@ -1,55 +0,0 @@ -import { Accounts } from 'meteor/accounts-base'; -import { Match, check } from 'meteor/check'; -import { Meteor } from 'meteor/meteor'; -import { ServiceConfiguration } from 'meteor/service-configuration'; -import _ from 'underscore'; - -const AccessTokenServices = {}; - -export const registerAccessTokenService = function (serviceName, handleAccessTokenRequest) { - AccessTokenServices[serviceName] = { - serviceName, - handleAccessTokenRequest, - }; -}; - -// Listen to calls to `login` with an oauth option set. This is where -// users actually get logged in to meteor via oauth. -Accounts.registerLoginHandler(async (options) => { - if (!options.accessToken) { - return undefined; // don't handle - } - - check( - options, - Match.ObjectIncluding({ - serviceName: String, - }), - ); - - const service = AccessTokenServices[options.serviceName]; - - // Skip everything if there's no service set by the oauth middleware - if (!service) { - throw new Error(`Unexpected AccessToken service ${options.serviceName}`); - } - - // Make sure we're configured - if (!(await ServiceConfiguration.configurations.findOneAsync({ service: options.serviceName }))) { - throw new Accounts.ConfigError(); - } - - if (!_.contains(Accounts.oauth.serviceNames(), service.serviceName)) { - // serviceName was not found in the registered services list. - // This could happen because the service never registered itself or - // unregisterService was called on it. - return { - type: 'oauth', - error: new Meteor.Error(Accounts.LoginCancelledError.numericError, `No registered oauth service found for: ${service.serviceName}`), - }; - } - - const oauthResult = await service.handleAccessTokenRequest(options); - - return Accounts.updateOrCreateUserFromExternalService(service.serviceName, oauthResult.serviceData, oauthResult.options); -}); diff --git a/apps/meteor/app/lib/server/oauth/oauth.ts b/apps/meteor/app/lib/server/oauth/oauth.ts new file mode 100644 index 0000000000000..8413fdce6e55c --- /dev/null +++ b/apps/meteor/app/lib/server/oauth/oauth.ts @@ -0,0 +1,65 @@ +import { Accounts } from 'meteor/accounts-base'; +import { Match, check } from 'meteor/check'; +import { Meteor } from 'meteor/meteor'; +import { ServiceConfiguration } from 'meteor/service-configuration'; + +const AccessTokenServices: Record< + string, + { + serviceName: string; + handleAccessTokenRequest: (options: any) => Promise<{ serviceData: any; options: any }>; + } +> = {}; + +export const registerAccessTokenService = function ( + serviceName: string, + handleAccessTokenRequest: (options: any) => Promise<{ serviceData: any; options: any }>, +): void { + AccessTokenServices[serviceName] = { + serviceName, + handleAccessTokenRequest, + }; +}; + +// Listen to calls to `login` with an oauth option set. This is where +// users actually get logged in to meteor via oauth. +Accounts.registerLoginHandler((options) => { + if (!options.accessToken) { + return undefined; // don't handle + } + + check( + options, + Match.ObjectIncluding({ + serviceName: String, + }), + ); + + const service = AccessTokenServices[options.serviceName]; + + // Skip everything if there's no service set by the oauth middleware + if (!service) { + throw new Error(`Unexpected AccessToken service ${options.serviceName}`); + } + + // Make sure we're configured + return ServiceConfiguration.configurations.findOneAsync({ service: options.serviceName }).then((config) => { + if (!config) { + throw new Accounts.ConfigError(); + } + + if (!(Accounts.oauth.serviceNames() as string[]).includes(service.serviceName)) { + // serviceName was not found in the registered services list. + // This could happen because the service never registered itself or + // unregisterService was called on it. + return { + type: 'oauth', + error: new Meteor.Error(Accounts.LoginCancelledError.numericError, `No registered oauth service found for: ${service.serviceName}`), + }; + } + + return service.handleAccessTokenRequest(options).then((oauthResult) => { + return Accounts.updateOrCreateUserFromExternalService(service.serviceName, oauthResult.serviceData, oauthResult.options); + }); + }) as any; +}); From 03c460d04424fe4280cc663e44cc9bb389859f64 Mon Sep 17 00:00:00 2001 From: Tasso Date: Thu, 29 Jan 2026 17:46:38 -0300 Subject: [PATCH 54/80] refactor: convert `apps/meteor/app/lib/server/oauth/{facebook,google,twitter}.js` to TypeScript --- .../server/oauth/{facebook.js => facebook.ts} | 20 ++++++----- .../lib/server/oauth/{google.js => google.ts} | 33 +++++++++++-------- .../server/oauth/{twitter.js => twitter.ts} | 26 ++++++++++----- 3 files changed, 49 insertions(+), 30 deletions(-) rename apps/meteor/app/lib/server/oauth/{facebook.js => facebook.ts} (71%) rename apps/meteor/app/lib/server/oauth/{google.js => google.ts} (62%) rename apps/meteor/app/lib/server/oauth/{twitter.js => twitter.ts} (63%) diff --git a/apps/meteor/app/lib/server/oauth/facebook.js b/apps/meteor/app/lib/server/oauth/facebook.ts similarity index 71% rename from apps/meteor/app/lib/server/oauth/facebook.js rename to apps/meteor/app/lib/server/oauth/facebook.ts index e60dff901d69c..b8a2f6cd5fe19 100644 --- a/apps/meteor/app/lib/server/oauth/facebook.js +++ b/apps/meteor/app/lib/server/oauth/facebook.ts @@ -3,7 +3,6 @@ import crypto from 'crypto'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { OAuth } from 'meteor/oauth'; -import _ from 'underscore'; import { registerAccessTokenService } from './oauth'; @@ -12,7 +11,7 @@ const whitelisted = ['id', 'email', 'name', 'first_name', 'last_name', 'link', ' const FB_API_VERSION = 'v2.9'; const FB_URL = 'https://graph.facebook.com'; -const getIdentity = async function (accessToken, fields, secret) { +const getIdentity = async function (accessToken: string, fields: string[], secret: string): Promise> { const hmac = crypto.createHmac('sha256', OAuth.openSecret(secret)); hmac.update(accessToken); @@ -30,8 +29,8 @@ const getIdentity = async function (accessToken, fields, secret) { } return request.json(); - } catch (err) { - throw _.extend(new Error(`Failed to fetch identity from Facebook. ${err.message}`), { + } catch (err: any) { + throw Object.assign(new Error(`Failed to fetch identity from Facebook. ${err.message}`), { response: err.message, }); } @@ -49,13 +48,18 @@ registerAccessTokenService('facebook', async (options) => { const identity = await getIdentity(options.accessToken, whitelisted, options.secret); - const serviceData = { + const serviceData: Record = { accessToken: options.accessToken, - expiresAt: +new Date() + 1000 * parseInt(options.expiresIn, 10), + expiresAt: +new Date() + 1000 * options.expiresIn, }; - const fields = _.pick(identity, whitelisted); - _.extend(serviceData, fields); + const fields: Record = {}; + for (const key of whitelisted) { + if (identity[key]) { + fields[key] = identity[key]; + } + } + Object.assign(serviceData, fields); return { serviceData, diff --git a/apps/meteor/app/lib/server/oauth/google.js b/apps/meteor/app/lib/server/oauth/google.ts similarity index 62% rename from apps/meteor/app/lib/server/oauth/google.js rename to apps/meteor/app/lib/server/oauth/google.ts index 34c626d6a9417..33aebdbc0ef6e 100644 --- a/apps/meteor/app/lib/server/oauth/google.js +++ b/apps/meteor/app/lib/server/oauth/google.ts @@ -1,11 +1,10 @@ import { serverFetch as fetch } from '@rocket.chat/server-fetch'; import { Match, check } from 'meteor/check'; import { Google } from 'meteor/google-oauth'; -import _ from 'underscore'; import { registerAccessTokenService } from './oauth'; -async function getIdentity(accessToken) { +async function getIdentity(accessToken: string): Promise> { try { const request = await fetch('https://www.googleapis.com/oauth2/v1/userinfo', { params: { access_token: accessToken }, @@ -16,14 +15,14 @@ async function getIdentity(accessToken) { } return request.json(); - } catch (err) { - throw _.extend(new Error(`Failed to fetch identity from Google. ${err.message}`), { + } catch (err: any) { + throw Object.assign(new Error(`Failed to fetch identity from Google. ${err.message}`), { response: err.message, }); } } -async function getScopes(accessToken) { +async function getScopes(accessToken: string): Promise { try { const request = await fetch('https://www.googleapis.com/oauth2/v1/tokeninfo', { params: { access_token: accessToken }, @@ -34,8 +33,8 @@ async function getScopes(accessToken) { } return (await request.json()).scope.split(' '); - } catch (err) { - throw _.extend(new Error(`Failed to fetch tokeninfo from Google. ${err.message}`), { + } catch (err: any) { + throw Object.assign(new Error(`Failed to fetch tokeninfo from Google. ${err.message}`), { response: err.message, }); } @@ -55,21 +54,27 @@ registerAccessTokenService('google', async (options) => { const identity = await getIdentity(options.accessToken); - const serviceData = { + const serviceData: Record = { accessToken: options.accessToken, idToken: options.idToken, - expiresAt: +new Date() + 1000 * parseInt(options.expiresIn, 10), - scope: options.scopes || (await getScopes(options.accessToken)), + expiresAt: +new Date() + 1000 * options.expiresIn, + scope: options.scope || (await getScopes(options.accessToken)), }; - const fields = _.pick(identity, Google.whitelistedFields); - _.extend(serviceData, fields); + const fields: Record = {}; + const { whitelistedFields } = Google as any; + for (const key of whitelistedFields) { + if (identity[key]) { + fields[key] = identity[key]; + } + } + Object.assign(serviceData, fields); // only set the token in serviceData if it's there. this ensures // that we don't lose old ones (since we only get this on the first // log in attempt) - if (options.refreshToken) { - serviceData.refreshToken = options.refreshToken; + if ((options as any).refreshToken) { + serviceData.refreshToken = (options as any).refreshToken; } return { diff --git a/apps/meteor/app/lib/server/oauth/twitter.js b/apps/meteor/app/lib/server/oauth/twitter.ts similarity index 63% rename from apps/meteor/app/lib/server/oauth/twitter.js rename to apps/meteor/app/lib/server/oauth/twitter.ts index b3394332a037e..d62b784f024d7 100644 --- a/apps/meteor/app/lib/server/oauth/twitter.js +++ b/apps/meteor/app/lib/server/oauth/twitter.ts @@ -1,12 +1,17 @@ import { Match, check } from 'meteor/check'; +// @ts-expect-error - twit has no type definitions import Twit from 'twit'; -import _ from 'underscore'; import { registerAccessTokenService } from './oauth'; const whitelistedFields = ['id', 'name', 'description', 'profile_image_url', 'profile_image_url_https', 'lang', 'email']; -const getIdentity = async function (accessToken, appId, appSecret, accessTokenSecret) { +const getIdentity = async function ( + accessToken: string, + appId: string, + appSecret: string, + accessTokenSecret: string, +): Promise> { const Twitter = new Twit({ consumer_key: appId, consumer_secret: appSecret, @@ -17,8 +22,8 @@ const getIdentity = async function (accessToken, appId, appSecret, accessTokenSe const result = await Twitter.get('account/verify_credentials.json?include_email=true'); return result.data; - } catch (err) { - throw _.extend(new Error(`Failed to fetch identity from Twwiter. ${err.message}`), { + } catch (err: any) { + throw Object.assign(new Error(`Failed to fetch identity from Twwiter. ${err.message}`), { response: err.response, }); } @@ -38,13 +43,18 @@ registerAccessTokenService('twitter', async (options) => { const identity = await getIdentity(options.accessToken, options.appId, options.appSecret, options.accessTokenSecret); - const serviceData = { + const serviceData: Record = { accessToken: options.accessToken, - expiresAt: +new Date() + 1000 * parseInt(options.expiresIn, 10), + expiresAt: +new Date() + 1000 * options.expiresIn, }; - const fields = _.pick(identity, whitelistedFields); - _.extend(serviceData, fields); + const fields: Record = {}; + for (const key of whitelistedFields) { + if (identity[key]) { + fields[key] = identity[key]; + } + } + Object.assign(serviceData, fields); return { serviceData, From 8c25a38805093442ab40c4040e747e0df2a5d8ed Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 30 Jan 2026 15:45:20 -0300 Subject: [PATCH 55/80] refactor: convert `apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js` to TypeScript --- ...anned-responses.js => canned-responses.ts} | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) rename apps/meteor/ee/app/api-enterprise/server/lib/{canned-responses.js => canned-responses.ts} (76%) diff --git a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.ts similarity index 76% rename from apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js rename to apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.ts index 7bd1ad08c6b4d..48abcad1ba752 100644 --- a/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.js +++ b/apps/meteor/ee/app/api-enterprise/server/lib/canned-responses.ts @@ -1,10 +1,11 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { CannedResponse } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { hasPermissionAsync } from '../../../../../app/authorization/server/functions/hasPermission'; import { getDepartmentsWhichUserCanAccess } from '../../../livechat-enterprise/server/api/lib/departments'; -export async function findAllCannedResponses({ userId }) { +export async function findAllCannedResponses({ userId }: { userId: IUser['_id'] }) { // If the user is an admin or livechat manager, get his own responses and all responses from all departments if (await hasPermissionAsync(userId, 'view-all-canned-responses')) { const cannedResponses = await CannedResponse.find({ @@ -48,27 +49,35 @@ export async function findAllCannedResponses({ userId }) { return cannedResponses; } -/** - * @param {Object} param0 - * @param {String} param0.userId - * @param {String} [param0.shortcut] - * @param {String} [param0.text] - * @param {String} [param0.departmentId] - * @param {String} [param0.scope] - * @param {String} [param0.createdBy] - * @param {String[]} [param0.tags] - * @param {Object} param0.options - * @param {Number} param0.options.offset - * @param {Number} param0.options.count - * @param {Object} param0.options.sort - * @param {Object} param0.options.fields - */ -export async function findAllCannedResponsesFilter({ userId, shortcut, text, departmentId, scope, createdBy, tags = [], options = {} }) { - let extraFilter = []; +export async function findAllCannedResponsesFilter({ + userId, + shortcut, + text, + departmentId, + scope, + createdBy, + tags = [], + options = {}, +}: { + userId: IUser['_id']; + shortcut?: string; + text?: string; + departmentId?: string; + scope?: string; + createdBy?: string; + tags?: string[]; + options?: { + offset?: number; + count?: number; + sort?: Record; + fields?: Record; + }; +}) { + let extraFilter: Record[] = []; // if user cannot see all, filter to private + public + departments user is in if (!(await hasPermissionAsync(userId, 'view-all-canned-responses'))) { const accessibleDepartments = await getDepartmentsWhichUserCanAccess(userId, true); - const isDepartmentInScope = (departmentId) => !!accessibleDepartments.includes(departmentId); + const isDepartmentInScope = (departmentId: string) => !!accessibleDepartments.includes(departmentId); const departmentIds = departmentId && isDepartmentInScope(departmentId) ? [departmentId] : accessibleDepartments; @@ -101,9 +110,9 @@ export async function findAllCannedResponsesFilter({ userId, shortcut, text, dep ]; } - const textFilter = new RegExp(escapeRegExp(text), 'i'); + const textFilter = new RegExp(escapeRegExp(text!), 'i'); - let filter = { + let filter: Record = { $and: [ ...(shortcut ? [{ shortcut }] : []), ...(text ? [{ $or: [{ shortcut: textFilter }, { text: textFilter }] }] : []), @@ -138,7 +147,7 @@ export async function findAllCannedResponsesFilter({ userId, shortcut, text, dep }; } -export async function findOneCannedResponse({ userId, _id }) { +export async function findOneCannedResponse({ userId, _id }: { userId: IUser['_id']; _id: string }) { if (await hasPermissionAsync(userId, 'view-all-canned-responses')) { return CannedResponse.findOneById(_id); } From 9b3d36bccbc24c0e6870529f17f5c2573c87755a Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 30 Jan 2026 16:35:53 -0300 Subject: [PATCH 56/80] refactor: convert `apps/meteor/ee/server/apps/orchestrator.js` to TypeScript --- .../app/apps/server/converters/threads.ts | 25 +--- .../ee/server/apps/communication/uikit.ts | 4 +- .../server/apps/communication/websockets.ts | 18 +-- .../apps/{orchestrator.js => orchestrator.ts} | 113 +++++++++++------- 4 files changed, 87 insertions(+), 73 deletions(-) rename apps/meteor/ee/server/apps/{orchestrator.js => orchestrator.ts} (75%) diff --git a/apps/meteor/app/apps/server/converters/threads.ts b/apps/meteor/app/apps/server/converters/threads.ts index d6284688b984a..8b2d22be6b64a 100644 --- a/apps/meteor/app/apps/server/converters/threads.ts +++ b/apps/meteor/app/apps/server/converters/threads.ts @@ -1,4 +1,4 @@ -import type { IAppRoomsConverter, IAppThreadsConverter, IAppUsersConverter, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; +import type { IAppServerOrchestrator, IAppThreadsConverter, IAppUsersConverter, IAppsMessage, IAppsUser } from '@rocket.chat/apps'; import type { IMessage as AppsEngineMessage, IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages'; import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; import { isEditedMessage } from '@rocket.chat/core-typings'; @@ -8,25 +8,8 @@ import { Messages } from '@rocket.chat/models'; import { cachedFunction } from './cachedFunction'; import { transformMappedData } from './transformMappedData'; -// eslint-disable-next-line @typescript-eslint/naming-convention -interface Orchestrator { - rooms: () => { - convertById: IAppRoomsConverter['convertById']; - }; - users: () => { - convertById: IAppUsersConverter['convertById']; - convertToApp: IAppUsersConverter['convertToApp']; - }; -} - export class AppThreadsConverter implements IAppThreadsConverter { - constructor( - private readonly orch: { - getConverters: () => { - get: (key: O) => ReturnType; - }; - }, - ) { + constructor(private readonly orch: IAppServerOrchestrator) { this.orch = orch; } @@ -66,8 +49,8 @@ export class AppThreadsConverter implements IAppThreadsConverter { async convertMessage( msgObj: IMessage, room: IRoom, - convertUserById: ReturnType['convertById'], - convertToApp: ReturnType['convertToApp'], + convertUserById: IAppUsersConverter['convertById'], + convertToApp: IAppUsersConverter['convertToApp'], ): Promise { const map = { id: '_id', diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index e5e851ef8f2b1..4697aa35a48e5 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -64,7 +64,9 @@ router.use(async (req: Request, res, next) => { const { 'x-visitor-token': visitorToken } = req.headers; if (visitorToken) { - req.body.visitor = await Apps.getConverters()?.get('visitors').convertByToken(visitorToken); + req.body.visitor = await Apps.getConverters() + ?.get('visitors') + .convertByToken(visitorToken as string); // FIXME might be string[] } if (!req.user && !req.body.visitor) { diff --git a/apps/meteor/ee/server/apps/communication/websockets.ts b/apps/meteor/ee/server/apps/communication/websockets.ts index e6fc97464b8da..355404fca9f16 100644 --- a/apps/meteor/ee/server/apps/communication/websockets.ts +++ b/apps/meteor/ee/server/apps/communication/websockets.ts @@ -1,17 +1,17 @@ +import type { IAppServerOrchestrator } from '@rocket.chat/apps'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; import { api } from '@rocket.chat/core-services'; import type { IStreamer } from 'meteor/rocketchat:streamer'; +import { AppEvents } from './events'; import notifications from '../../../../app/notifications/server/lib/Notifications'; import { SystemLogger } from '../../../../server/lib/logger/system'; -import type { AppServerOrchestrator } from '../orchestrator'; -import { AppEvents } from './events'; export { AppEvents }; export class AppServerListener { - private orch: AppServerOrchestrator; + private orch: IAppServerOrchestrator; engineStreamer: IStreamer<'apps-engine'>; @@ -20,7 +20,7 @@ export class AppServerListener { received; constructor( - orch: AppServerOrchestrator, + orch: IAppServerOrchestrator, engineStreamer: IStreamer<'apps-engine'>, clientStreamer: IStreamer<'apps'>, received: Map, @@ -90,13 +90,13 @@ export class AppServerListener { const storageItem = await this.orch.getStorage()!.retrieveOne(appId); - const appPackage = await this.orch.getAppSourceStorage()!.fetch(storageItem); + const appPackage = await this.orch.getAppSourceStorage()!.fetch(storageItem!); - const isEnabled = AppStatusUtils.isEnabled(storageItem.status); + const isEnabled = AppStatusUtils.isEnabled(storageItem!.status); if (isEnabled) { - await this.orch.getManager()!.updateAndStartupLocal(storageItem, appPackage); + await this.orch.getManager()!.updateAndStartupLocal(storageItem!, appPackage); } else { - await this.orch.getManager()!.updateAndInitializeLocal(storageItem, appPackage); + await this.orch.getManager()!.updateAndInitializeLocal(storageItem!, appPackage); } this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); @@ -143,7 +143,7 @@ export class AppServerNotifier { listener: AppServerListener; - constructor(orch: AppServerOrchestrator) { + constructor(orch: IAppServerOrchestrator) { this.engineStreamer = notifications.streamAppsEngine; // This is used to broadcast to the web clients diff --git a/apps/meteor/ee/server/apps/orchestrator.js b/apps/meteor/ee/server/apps/orchestrator.ts similarity index 75% rename from apps/meteor/ee/server/apps/orchestrator.js rename to apps/meteor/ee/server/apps/orchestrator.ts index d2ed01062ce1c..ce6d9f7bab5e2 100644 --- a/apps/meteor/ee/server/apps/orchestrator.js +++ b/apps/meteor/ee/server/apps/orchestrator.ts @@ -3,8 +3,11 @@ import * as os from 'os'; import * as path from 'path'; import { registerOrchestrator } from '@rocket.chat/apps'; +import type { IAppConvertersMap, IAppServerOrchestrator } from '@rocket.chat/apps'; import { EssentialAppDisabledException } from '@rocket.chat/apps-engine/definition/exceptions'; import { AppManager } from '@rocket.chat/apps-engine/server/AppManager'; +import type { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp'; +import { AppInstallationSource } from '@rocket.chat/apps-engine/server/storage'; import { Logger } from '@rocket.chat/logger'; import { AppLogs, Apps as AppsModel, AppsPersistence, Statistics } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -14,6 +17,7 @@ import { MarketplaceAPIClient } from './marketplace/MarketplaceAPIClient'; import { isTesting } from './marketplace/isTesting'; import { AppRealLogStorage, AppRealStorage, ConfigurableAppSourceStorage } from './storage'; import { RealAppBridges } from '../../../app/apps/server/bridges'; +import type { AppSchedulerBridge } from '../../../app/apps/server/bridges/scheduler'; import { AppMessagesConverter, AppRoomsConverter, @@ -32,9 +36,41 @@ import { canEnableApp } from '../../app/license/server/canEnableApp'; const DISABLED_PRIVATE_APP_INSTALLATION = ['yes', 'true'].includes(String(process.env.DISABLE_PRIVATE_APP_INSTALLATION).toLowerCase()); -export class AppServerOrchestrator { +export class AppServerOrchestrator implements IAppServerOrchestrator { + _isInitialized: boolean; + + private _rocketchatLogger!: Logger; + + private _model: typeof AppsModel; + + private _logModel: typeof AppLogs; + + private _persistModel: typeof AppsPersistence; + + private _statisticsModel: typeof Statistics; + + private _storage!: AppRealStorage; + + private _logStorage!: AppRealLogStorage; + + private _appSourceStorage!: ConfigurableAppSourceStorage; + + private _converters!: IAppConvertersMap; + + private _bridges!: RealAppBridges; + + private _manager!: AppManager; + + private _communicators!: Map; + + public marketplaceClient: MarketplaceAPIClient; + constructor() { this._isInitialized = false; + this._model = AppsModel; + this._logModel = AppLogs; + this._persistModel = AppsPersistence; + this._statisticsModel = Statistics; this.marketplaceClient = new MarketplaceAPIClient(); } @@ -57,18 +93,18 @@ export class AppServerOrchestrator { settings.get('Apps_Framework_Source_Package_Storage_FileSystem_Path'), ); - this._converters = new Map(); + this._converters = new Map() as IAppConvertersMap; this._converters.set('messages', new AppMessagesConverter(this)); this._converters.set('rooms', new AppRoomsConverter(this)); this._converters.set('settings', new AppSettingsConverter(this)); this._converters.set('users', new AppUsersConverter(this)); this._converters.set('visitors', new AppVisitorsConverter(this)); - this._converters.set('contacts', new AppContactsConverter(this)); + this._converters.set('contacts', new AppContactsConverter()); this._converters.set('departments', new AppDepartmentsConverter(this)); this._converters.set('uploads', new AppUploadsConverter(this)); this._converters.set('videoConferences', new AppVideoConferencesConverter()); this._converters.set('threads', new AppThreadsConverter(this)); - this._converters.set('roles', new AppRolesConverter(this)); + this._converters.set('roles', new AppRolesConverter()); this._bridges = new RealAppBridges(this); @@ -77,7 +113,7 @@ export class AppServerOrchestrator { try { // We call this only once at server startup, so using the synchronous version is fine fs.mkdirSync(tempFilePath); - } catch (err) { + } catch (err: any) { // If the temp directory already exists, we can continue if (err.code !== 'EEXIST') { throw new Error('Failed to initialize the Apps-Engine', { cause: err }); @@ -108,9 +144,6 @@ export class AppServerOrchestrator { return this._model; } - /** - * @returns {AppsPersistenceModel} - */ getPersistenceModel() { return this._persistModel; } @@ -171,16 +204,14 @@ export class AppServerOrchestrator { return DISABLED_PRIVATE_APP_INSTALLATION; } - /** - * @returns {Logger} - */ getRocketChatLogger() { return this._rocketchatLogger; } - debugLog(...args) { + debugLog(...args: any[]) { if (this.isDebugging()) { - this.getRocketChatLogger().debug(...args); + // FIXME: Logger.debug expects only one argument, but the method signature allows multiple + this.getRocketChatLogger().debug(...(args as [any])); } } @@ -202,7 +233,7 @@ export class AppServerOrchestrator { await canEnableApp(app.getStorageItem()); await this.getManager().loadOne(app.getID(), true); - } catch (error) { + } catch (error: any) { this._rocketchatLogger.warn({ msg: 'App could not be enabled', appName: app.getInfo().name, @@ -211,7 +242,7 @@ export class AppServerOrchestrator { } } - await this.getBridges().getSchedulerBridge().startScheduler(); + await (this.getBridges().getSchedulerBridge() as AppSchedulerBridge).startScheduler(); const appCount = (await this.getManager().get({ enabled: true })).length; @@ -222,14 +253,14 @@ export class AppServerOrchestrator { } async migratePrivateApps() { - const apps = await this.getManager().get({ installationSource: 'private' }); + const apps = await this.getManager().get({ installationSource: AppInstallationSource.PRIVATE }); await Promise.all(apps.map((app) => this.getManager().migrate(app.getID()))); await Promise.all(apps.map((app) => this.getNotifier().appUpdated(app.getID()))); } - async findMajorVersionUpgradeDate(targetVersion = 7) { - let upgradeToV7Date = null; + async findMajorVersionUpgradeDate(targetVersion = 7): Promise { + let upgradeToV7Date: Date | null = null; let hadPreTargetVersion = false; try { @@ -239,7 +270,9 @@ export class AppServerOrchestrator { return upgradeToV7Date; } - const statsAscendingByInstallDate = statistics.sort((a, b) => new Date(a.installedAt) - new Date(b.installedAt)); + const statsAscendingByInstallDate = statistics.sort( + (a, b) => new Date(a.installedAt || 0).getTime() - new Date(b.installedAt || 0).getTime(), + ); for (const stat of statsAscendingByInstallDate) { const version = stat.version || ''; @@ -257,7 +290,7 @@ export class AppServerOrchestrator { } if (hadPreTargetVersion && majorVersion >= targetVersion) { - upgradeToV7Date = new Date(stat.installedAt); + upgradeToV7Date = new Date(stat.installedAt!); this._rocketchatLogger.info({ msg: 'Found upgrade to target version date', targetVersion, @@ -266,7 +299,7 @@ export class AppServerOrchestrator { break; } } - } catch (err) { + } catch (err: any) { this._rocketchatLogger.error({ msg: 'Error checking statistics for version history', err, @@ -277,34 +310,34 @@ export class AppServerOrchestrator { } async disableMarketplaceApps() { - return this.disableApps('marketplace', false, 5); + return this.disableApps(AppInstallationSource.MARKETPLACE, false, 5); } async disablePrivateApps() { - return this.disableApps('private', true, 0); + return this.disableApps(AppInstallationSource.PRIVATE, true, 0); } - async disableApps(installationSource, grandfatherApps, maxApps) { + async disableApps(installationSource: AppInstallationSource, grandfatherApps: boolean, maxApps: number): Promise { const upgradeToV7Date = await this.findMajorVersionUpgradeDate(); const apps = await this.getManager().get({ installationSource }); - const grandfathered = []; - const toKeep = []; - const toDisable = []; + const grandfathered: ProxiedApp[] = []; + const toKeep: ProxiedApp[] = []; + const toDisable: ProxiedApp[] = []; for (const app of apps) { const storageItem = app.getStorageItem(); const isEnabled = ['enabled', 'manually_enabled', 'auto_enabled'].includes(storageItem.status); - const marketplaceInfo = storageItem.marketplaceInfo && storageItem.marketplaceInfo[0]; + const marketplaceInfo = storageItem.marketplaceInfo?.[0]; - const wasInstalledBeforeV7 = upgradeToV7Date && storageItem.createdAt && new Date(storageItem.createdAt) < upgradeToV7Date; + const wasInstalledBeforeV7 = upgradeToV7Date && storageItem.createdAt && new Date(storageItem.createdAt || 0) < upgradeToV7Date; if (wasInstalledBeforeV7 && isEnabled && grandfatherApps) { grandfathered.push(app); continue; } - if (marketplaceInfo?.isEnterpriseOnly === true && installationSource === 'marketplace') { + if (marketplaceInfo?.isEnterpriseOnly === true && installationSource === AppInstallationSource.MARKETPLACE) { toDisable.push(app); continue; } @@ -314,7 +347,7 @@ export class AppServerOrchestrator { } } - toKeep.sort((a, b) => new Date(a.getStorageItem().createdAt || 0) - new Date(b.getStorageItem().createdAt || 0)); + toKeep.sort((a, b) => new Date(a.getStorageItem().createdAt || 0).getTime() - new Date(b.getStorageItem().createdAt || 0).getTime()); if (toKeep.length > maxApps) { toDisable.push(...toKeep.splice(maxApps)); @@ -337,7 +370,7 @@ export class AppServerOrchestrator { keptCount: grandfathered.length + toKeep.length, disabledCount: toDisable.length, }); - } catch (error) { + } catch (error: any) { this._rocketchatLogger.error({ msg: 'Error disabling apps', err: error, @@ -352,10 +385,9 @@ export class AppServerOrchestrator { return; } - return this._manager - .unload() + return (this._manager.unload as any)() .then(() => this._rocketchatLogger.info('Unloaded the Apps Framework.')) - .catch((err) => + .catch((err: any) => this._rocketchatLogger.error({ msg: 'Failed to unload the Apps Framework!', err, @@ -363,7 +395,7 @@ export class AppServerOrchestrator { ); } - async updateAppsMarketplaceInfo(apps = []) { + async updateAppsMarketplaceInfo(apps: any[] = []) { if (!this.isLoaded()) { return; } @@ -371,10 +403,7 @@ export class AppServerOrchestrator { return this._manager.updateAppsMarketplaceInfo(apps).then(() => this._manager.get()); } - /** - * @returns {Promise} - */ - async installedApps(filter = {}) { + async installedApps(filter: Record = {}): Promise { if (!this.isLoaded()) { return; } @@ -382,7 +411,7 @@ export class AppServerOrchestrator { return this._manager.get(filter); } - async triggerEvent(event, ...payload) { + async triggerEvent(event: string, ...payload: any[]) { if (!this.isLoaded()) { return; } @@ -390,7 +419,7 @@ export class AppServerOrchestrator { return this.getBridges() .getListenerBridge() .handleEvent({ event, payload }) - .catch((error) => { + .catch((error: any) => { if (error instanceof EssentialAppDisabledException) { throw new Meteor.Error('error-essential-app-disabled'); } From 2108b9d019c31f2b4e464f5a4bbb10e4d7dc7ac7 Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 30 Jan 2026 17:01:11 -0300 Subject: [PATCH 57/80] refactor: convert `apps/meteor/ee/lib/misc/determineFileType.js` to TypeScript --- apps/meteor/app/apps/server/bridges/uploads.ts | 2 +- .../lib/misc/{determineFileType.js => determineFileType.ts} | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename apps/meteor/ee/lib/misc/{determineFileType.js => determineFileType.ts} (59%) diff --git a/apps/meteor/app/apps/server/bridges/uploads.ts b/apps/meteor/app/apps/server/bridges/uploads.ts index b9d0ff67de58e..d1a5d1c492ef0 100644 --- a/apps/meteor/app/apps/server/bridges/uploads.ts +++ b/apps/meteor/app/apps/server/bridges/uploads.ts @@ -51,7 +51,7 @@ export class AppUploadBridge extends UploadBridge { const fileStore = FileUpload.getStore('Uploads'); - details.type = determineFileType(buffer, details.name); + details.type = await determineFileType(buffer, details.name); const uploadedFile = await fileStore.insert(getUploadDetails(details), buffer); this.orch.debugLog(`The App ${appId} has created an upload`, uploadedFile); diff --git a/apps/meteor/ee/lib/misc/determineFileType.js b/apps/meteor/ee/lib/misc/determineFileType.ts similarity index 59% rename from apps/meteor/ee/lib/misc/determineFileType.js rename to apps/meteor/ee/lib/misc/determineFileType.ts index 0942f78032ca2..afdf290fa5592 100644 --- a/apps/meteor/ee/lib/misc/determineFileType.js +++ b/apps/meteor/ee/lib/misc/determineFileType.ts @@ -1,15 +1,15 @@ -import fileType from 'file-type'; +import { fromBuffer } from 'file-type'; import { mime as MIME } from '../../../app/utils/lib/mimeTypes'; -export function determineFileType(buffer, name) { +export async function determineFileType(buffer: Buffer, name: string): Promise { const mime = MIME.lookup(name); if (mime) { return Array.isArray(mime) ? mime[0] : mime; } - const detectedType = fileType(buffer); + const detectedType = await fromBuffer(buffer); if (detectedType) { return detectedType.mime; From fb250a36dfdbeaac4561dd093d98f387d3f12739 Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 30 Jan 2026 17:53:31 -0300 Subject: [PATCH 58/80] refactor: convert `apps/meteor/server/configuration/accounts_meld.js` to TypeScript --- .../{accounts_meld.js => accounts_meld.ts} | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) rename apps/meteor/server/configuration/{accounts_meld.js => accounts_meld.ts} (52%) diff --git a/apps/meteor/server/configuration/accounts_meld.js b/apps/meteor/server/configuration/accounts_meld.ts similarity index 52% rename from apps/meteor/server/configuration/accounts_meld.js rename to apps/meteor/server/configuration/accounts_meld.ts index 037150def37ee..7db6887168e55 100644 --- a/apps/meteor/server/configuration/accounts_meld.js +++ b/apps/meteor/server/configuration/accounts_meld.ts @@ -2,18 +2,38 @@ import { Users } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import _ from 'underscore'; -export async function configureAccounts() { - const orig_updateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService; - Accounts.updateOrCreateUserFromExternalService = async function (serviceName, serviceData = {}, ...args /* , options*/) { +interface IEmailData { + address: string; + primary?: boolean; + verified: boolean; +} + +interface IServiceData { + _OAuthCustom?: boolean; + id?: string; + email?: string; + emailAddress?: string; + emails?: IEmailData[]; + [key: string]: any; +} + +export async function configureAccounts(): Promise { + const origUpdateOrCreateUserFromExternalService = Accounts.updateOrCreateUserFromExternalService; + Accounts.updateOrCreateUserFromExternalService = async function ( + this: any, + serviceName: string, + serviceData: IServiceData = {}, + ...args: any[] /* , options*/ + ) { const services = ['facebook', 'github', 'gitlab', 'google', 'meteor-developer', 'linkedin', 'twitter', 'apple']; if (services.includes(serviceName) === false && serviceData._OAuthCustom !== true) { - return orig_updateOrCreateUserFromExternalService.apply(this, [serviceName, serviceData, ...args]); + return origUpdateOrCreateUserFromExternalService.apply(this, [serviceName, serviceData, ...args] as any); } if (serviceName === 'meteor-developer') { if (Array.isArray(serviceData.emails)) { - const primaryEmail = serviceData.emails.sort((a) => a.primary !== true).filter((item) => item.verified === true)[0]; + const primaryEmail = serviceData.emails.sort((a) => (a.primary === true ? -1 : 1)).filter((item) => item.verified === true)[0]; serviceData.email = primaryEmail && primaryEmail.address; } } @@ -24,13 +44,13 @@ export async function configureAccounts() { if (serviceData.email) { const user = await Users.findOneByEmailAddress(serviceData.email); - if (user != null && user.services?.[serviceName]?.id !== serviceData.id) { + if (user != null && (user.services as any)?.[serviceName]?.id !== serviceData.id) { const findQuery = { address: serviceData.email, verified: true, }; - if (user.services?.password && !_.findWhere(user.emails, findQuery)) { + if (user.services?.password && !_.findWhere(user.emails as any, findQuery)) { await Users.resetPasswordAndSetRequirePasswordChange( user._id, true, @@ -38,11 +58,11 @@ export async function configureAccounts() { ); } - await Users.setServiceId(user._id, serviceName, serviceData.id); + await Users.setServiceId(user._id, serviceName, serviceData.id!); await Users.setEmailVerified(user._id, serviceData.email); } } - return orig_updateOrCreateUserFromExternalService.apply(this, [serviceName, serviceData, ...args]); - }; + return origUpdateOrCreateUserFromExternalService.apply(this, [serviceName, serviceData, ...args] as any); + } as any; } From 34f4122b84b8d8255164fac44f100b9ce92c81d6 Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 30 Jan 2026 18:18:19 -0300 Subject: [PATCH 59/80] refactor: convert `apps/meteor/server/lib/spotlight.js` to TypeScript --- .../server/lib/{spotlight.js => spotlight.ts} | 189 +++++++++++++----- 1 file changed, 143 insertions(+), 46 deletions(-) rename apps/meteor/server/lib/{spotlight.js => spotlight.ts} (53%) diff --git a/apps/meteor/server/lib/spotlight.js b/apps/meteor/server/lib/spotlight.ts similarity index 53% rename from apps/meteor/server/lib/spotlight.js rename to apps/meteor/server/lib/spotlight.ts index dc182ee9d3a6f..ecb9905515ce3 100644 --- a/apps/meteor/server/lib/spotlight.js +++ b/apps/meteor/server/lib/spotlight.ts @@ -1,17 +1,55 @@ import { Team } from '@rocket.chat/core-services'; +import type { IRoom, IUser, ITeam } from '@rocket.chat/core-typings'; import { Users, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { roomCoordinator } from './rooms/roomCoordinator'; import { canAccessRoomAsync, roomAccessAttributes } from '../../app/authorization/server'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; import { settings } from '../../app/settings/server'; import { trim } from '../../lib/utils/stringUtils'; import { readSecondaryPreferred } from '../database/readSecondaryPreferred'; -import { roomCoordinator } from './rooms/roomCoordinator'; + +interface ISearchRoomsParams { + userId: string | null | undefined; + text: string; + includeFederatedRooms?: boolean; +} + +interface ISearchUsersParams { + userId: string | null | undefined; + rid?: string; + text: string; + usernames: string[]; + mentions?: boolean; +} + +interface IUserSearchResult extends IUser { + outside?: boolean; + isTeam?: boolean; +} + +interface ITeamSearchResult extends ITeam { + isTeam: boolean; + username: string; + status: string; +} + +interface ISearchParams { + rid?: string; + text: string; + usernames: string[]; + options: any; + users: IUserSearchResult[]; + canListOutsiders?: boolean; + insiderExtraQuery?: Record[]; + mentions?: boolean; + match?: { startsWith: string | false; endsWith: string | false }; +} export class Spotlight { - async fetchRooms(userId, rooms) { - if (!settings.get('Store_Last_Message') || (await hasPermissionAsync(userId, 'preview-c-room'))) { + async fetchRooms(userId: string | null, rooms: IRoom[]): Promise { + if (!settings.get('Store_Last_Message') || (userId && (await hasPermissionAsync(userId, 'preview-c-room')))) { return rooms; } @@ -21,10 +59,10 @@ export class Spotlight { }); } - async searchRooms({ userId, text, includeFederatedRooms = false }) { + async searchRooms({ userId, text, includeFederatedRooms = false }: ISearchRoomsParams): Promise { const regex = new RegExp(trim(escapeRegExp(text)), 'i'); - const roomOptions = { + const roomOptions: any = { limit: 5, projection: { t: 1, @@ -33,7 +71,7 @@ export class Spotlight { teamMain: 1, joinCodeRequired: 1, lastMessage: 1, - federated: true, + federated: 1, prid: 1, }, sort: { @@ -46,7 +84,10 @@ export class Spotlight { return []; } - return this.fetchRooms(userId, await Rooms.findByNameAndTypeNotDefault(regex, 'c', roomOptions, includeFederatedRooms).toArray()); + return this.fetchRooms( + userId ?? null, + await Rooms.findByNameAndTypeNotDefault(regex, 'c', roomOptions, includeFederatedRooms).toArray(), + ); } if (!(await hasAllPermissionAsync(userId, ['view-outside-room', 'view-c-room']))) { @@ -56,27 +97,34 @@ export class Spotlight { const searchableRoomTypeIds = roomCoordinator.searchableRoomTypes(); const roomIds = ( - await SubscriptionsRaw.findByUserIdAndTypes(userId, searchableRoomTypeIds, { + await SubscriptionsRaw.findByUserIdAndTypes(userId, searchableRoomTypeIds as any, { projection: { rid: 1 }, }).toArray() ).map((s) => s.rid); - const exactRoom = await Rooms.findOneByNameAndType(text, searchableRoomTypeIds, roomOptions, includeFederatedRooms); + const exactRoom = await Rooms.findOneByNameAndType(text, searchableRoomTypeIds as any, roomOptions, includeFederatedRooms); if (exactRoom) { - roomIds.push(exactRoom.rid); + roomIds.push(exactRoom._id); } return this.fetchRooms( userId, - await Rooms.findByNameOrFNameAndTypesNotInIds(regex, searchableRoomTypeIds, roomIds, roomOptions, includeFederatedRooms).toArray(), + await Rooms.findByNameOrFNameAndTypesNotInIds( + regex, + searchableRoomTypeIds as any, + roomIds, + roomOptions, + includeFederatedRooms, + ).toArray(), ); } - mapOutsiders(u) { - u.outside = true; - return u; + mapOutsiders(u: IUser): IUserSearchResult { + const result = u as IUserSearchResult; + result.outside = true; + return result; } - processLimitAndUsernames(options, usernames, users) { + processLimitAndUsernames(options: any, usernames: string[], users: IUserSearchResult[]): IUserSearchResult[] | undefined { // Reduce the results from the limit for the next query options.limit -= users.length; @@ -86,46 +134,85 @@ export class Spotlight { } // Prevent the next query to get the same users - usernames.push(...users.map((u) => u.username).filter((u) => !usernames.includes(u))); + usernames.push(...users.map((u) => u.username).filter((u): u is string => !!u && !usernames.includes(u))); + + return undefined; } - async _searchInsiderUsers({ rid, text, usernames, options, users, insiderExtraQuery, match = { startsWith: false, endsWith: false } }) { + async _searchInsiderUsers({ + rid, + text, + usernames, + options, + users, + insiderExtraQuery, + match = { startsWith: false, endsWith: false }, + }: ISearchParams): Promise { // Get insiders first if (rid) { - const searchFields = settings.get('Accounts_SearchFields').trim().split(','); + const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - users.push(...(await Users.findByActiveUsersExcept(text, usernames, options, searchFields, insiderExtraQuery, match).toArray())); + users.push( + ...(await Users.findByActiveUsersExcept(text, usernames, options, searchFields, insiderExtraQuery, match as any).toArray()), + ); // If the limit was reached, return if (this.processLimitAndUsernames(options, usernames, users)) { return users; } } + + return undefined; } - async _searchConnectedUsers(userId, { text, usernames, options, users, match = { startsWith: false, endsWith: false } }, roomType) { - const searchFields = settings.get('Accounts_SearchFields').trim().split(','); + async _searchConnectedUsers( + userId: string, + { text, usernames, options, users, match = { startsWith: false, endsWith: false } }: ISearchParams, + roomType: string, + ): Promise { + const searchFields = settings.get('Accounts_SearchFields').trim().split(','); users.push( ...( - await SubscriptionsRaw.findConnectedUsersExcept(userId, text, usernames, searchFields, {}, options.limit || 5, roomType, match, { - readPreference: options.readPreference, - }) - ).map(this.mapOutsiders), + await SubscriptionsRaw.findConnectedUsersExcept( + userId, + text, + usernames, + searchFields, + {}, + options.limit || 5, + roomType as any, + match, + { + readPreference: options.readPreference, + }, + ) + ).map((u) => this.mapOutsiders(u as any)), ); // If the limit was reached, return if (this.processLimitAndUsernames(options, usernames, users)) { return users; } + + return undefined; } - async _searchOutsiderUsers({ text, usernames, options, users, canListOutsiders, match = { startsWith: false, endsWith: false } }) { + async _searchOutsiderUsers({ + text, + usernames, + options, + users, + canListOutsiders, + match = { startsWith: false, endsWith: false }, + }: ISearchParams): Promise { // Then get the outsiders if allowed if (canListOutsiders) { - const searchFields = settings.get('Accounts_SearchFields').trim().split(','); + const searchFields = settings.get('Accounts_SearchFields').trim().split(','); users.push( - ...(await Users.findByActiveUsersExcept(text, usernames, options, searchFields, undefined, match).toArray()).map(this.mapOutsiders), + ...(await Users.findByActiveUsersExcept(text, usernames, options, searchFields, undefined, match as any).toArray()).map((u) => + this.mapOutsiders(u), + ), ); // If the limit was reached, return @@ -133,18 +220,24 @@ export class Spotlight { return users; } } + + return undefined; } - mapTeams(teams) { + mapTeams(teams: ITeam[]): ITeamSearchResult[] { return teams.map((t) => { - t.isTeam = true; - t.username = t.name; - t.status = 'online'; - return t; + const result = t as ITeamSearchResult; + result.isTeam = true; + result.username = t.name; + result.status = 'online'; + return result; }); } - async _searchTeams(userId, { text, options, users, mentions }) { + async _searchTeams( + userId: string, + { text, options, users, mentions }: ISearchParams, + ): Promise<(IUserSearchResult | ITeamSearchResult)[] | undefined> { if (!mentions || settings.get('Troubleshoot_Disable_Teams_Mention')) { return users; } @@ -157,16 +250,20 @@ export class Spotlight { const teamOptions = { ...options, projection: { name: 1, type: 1 } }; const teams = await Team.search(userId, text, teamOptions); - users.push(...this.mapTeams(teams)); + (users as any).push(...this.mapTeams(teams)); return users; } - async searchUsers({ userId, rid, text, usernames, mentions }) { - const users = []; + async searchUsers({ userId, rid, text, usernames, mentions }: ISearchUsersParams): Promise { + if (!userId) { + return []; + } - const options = { - limit: settings.get('Number_of_users_autocomplete_suggestions'), + const users: IUserSearchResult[] = []; + + const options: any = { + limit: settings.get('Number_of_users_autocomplete_suggestions'), projection: { username: 1, nickname: 1, @@ -178,25 +275,25 @@ export class Spotlight { sort: { [settings.get('UI_Use_Real_Name') ? 'name' : 'username']: 1, }, - readPreference: readSecondaryPreferred(Users.col.s.db), + readPreference: readSecondaryPreferred((Users.col as any).s.db), }; - const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, uids: 1 } }); + const room = rid ? await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, uids: 1 } }) : null; if (rid && !room) { return users; } const canListOutsiders = await hasAllPermissionAsync(userId, ['view-outside-room', 'view-d-room']); - const canListInsiders = canListOutsiders || (rid && (await canAccessRoomAsync(room, { _id: userId }))); + const canListInsiders = canListOutsiders || (room && (await canAccessRoomAsync(room, { _id: userId }))); - const insiderExtraQuery = []; + const insiderExtraQuery: Record[] = []; - if (rid) { + if (rid && room) { switch (room.t) { case 'd': insiderExtraQuery.push({ - _id: { $in: room.uids.filter((id) => id !== userId) }, + _id: { $in: room.uids?.filter((id) => id !== userId) }, }); break; case 'l': @@ -214,7 +311,7 @@ export class Spotlight { } } - const searchParams = { + const searchParams: ISearchParams = { rid, text, usernames, From 8ebea35925146c58bd492d4834795a819816180e Mon Sep 17 00:00:00 2001 From: Tasso Date: Fri, 30 Jan 2026 18:35:10 -0300 Subject: [PATCH 60/80] refactor: migrate unit test `apps/meteor/tests/unit/app/statistics/server/lib/UAParserCustom.tests.js` --- apps/meteor/.mocharc.js | 2 +- .../server/lib/UAParserCustom.spec.ts} | 20 +++++++++---------- apps/meteor/jest.config.ts | 1 + 3 files changed, 11 insertions(+), 12 deletions(-) rename apps/meteor/{tests/unit/app/statistics/server/lib/UAParserCustom.tests.js => app/statistics/server/lib/UAParserCustom.spec.ts} (66%) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 0e9b30dffa699..519a6c5104010 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -28,7 +28,7 @@ module.exports = { 'tests/unit/server/**/*.spec.ts', 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', - 'app/statistics/server/**/*.spec.ts', + 'app/statistics/server/functions/**/*.spec.ts', 'app/livechat/server/lib/**/*.spec.ts', ], }; diff --git a/apps/meteor/tests/unit/app/statistics/server/lib/UAParserCustom.tests.js b/apps/meteor/app/statistics/server/lib/UAParserCustom.spec.ts similarity index 66% rename from apps/meteor/tests/unit/app/statistics/server/lib/UAParserCustom.tests.js rename to apps/meteor/app/statistics/server/lib/UAParserCustom.spec.ts index 24e961c53ebc7..8f8fbe701c591 100644 --- a/apps/meteor/tests/unit/app/statistics/server/lib/UAParserCustom.tests.js +++ b/apps/meteor/app/statistics/server/lib/UAParserCustom.spec.ts @@ -1,6 +1,4 @@ -import { expect } from 'chai'; - -import { UAParserMobile, UAParserDesktop } from '../../../../../../app/statistics/server/lib/UAParserCustom'; +import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; const UAMobile = 'RC Mobile; iOS 12.2; v3.4.0 (250)'; const UADesktop = @@ -11,19 +9,19 @@ const UAChrome = describe('UAParserCustom', () => { describe('UAParserMobile', () => { it('should identify mobile UA', () => { - expect(UAParserMobile.isMobileApp(UAMobile)).to.be.true; + expect(UAParserMobile.isMobileApp(UAMobile)).toBe(true); }); it('should not identify desktop UA', () => { - expect(UAParserMobile.isMobileApp(UADesktop)).to.be.false; + expect(UAParserMobile.isMobileApp(UADesktop)).toBe(false); }); it('should not identify chrome UA', () => { - expect(UAParserMobile.isMobileApp(UAChrome)).to.be.false; + expect(UAParserMobile.isMobileApp(UAChrome)).toBe(false); }); it('should parse mobile UA', () => { - expect(UAParserMobile.uaObject(UAMobile)).to.be.deep.equal({ + expect(UAParserMobile.uaObject(UAMobile)).toEqual({ device: { type: 'mobile-app', }, @@ -42,19 +40,19 @@ describe('UAParserCustom', () => { describe('UAParserDesktop', () => { it('should not identify mobile UA', () => { - expect(UAParserDesktop.isDesktopApp(UAMobile)).to.be.false; + expect(UAParserDesktop.isDesktopApp(UAMobile)).toBe(false); }); it('should identify desktop UA', () => { - expect(UAParserDesktop.isDesktopApp(UADesktop)).to.be.true; + expect(UAParserDesktop.isDesktopApp(UADesktop)).toBe(true); }); it('should not identify chrome UA', () => { - expect(UAParserDesktop.isDesktopApp(UAChrome)).to.be.false; + expect(UAParserDesktop.isDesktopApp(UAChrome)).toBe(false); }); it('should parse desktop UA', () => { - expect(UAParserDesktop.uaObject(UADesktop)).to.be.deep.equal({ + expect(UAParserDesktop.uaObject(UADesktop)).toEqual({ device: { type: 'desktop-app', }, diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index f8520778b1e49..4eda7577e1927 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -40,6 +40,7 @@ export default { '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', '/app/*/lib/**/*.spec.ts', + '/app/statistics/server/lib/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', '/server/services/import/**/*.spec.ts', '/server/settings/lib/**.spec.ts', From 99da65ab016513d7fd3e69349bcb72274e9d57c1 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 03:21:58 -0300 Subject: [PATCH 61/80] refactor: convert `apps/meteor/app/statistics/server/functions/sendUsageReport.spec.ts` test to Jest - Replace proxyquire with jest.mock() for module mocking - Replace sinon stubs with jest.fn() mock functions - Convert all assertions to Jest matchers - Remove file from Mocha configuration --- apps/meteor/.mocharc.js | 1 - .../server/functions/sendUsageReport.spec.ts | 114 ++++++++++-------- apps/meteor/jest.config.ts | 2 +- 3 files changed, 67 insertions(+), 50 deletions(-) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 519a6c5104010..f23763955406f 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -28,7 +28,6 @@ module.exports = { 'tests/unit/server/**/*.spec.ts', 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', - 'app/statistics/server/functions/**/*.spec.ts', 'app/livechat/server/lib/**/*.spec.ts', ], }; diff --git a/apps/meteor/app/statistics/server/functions/sendUsageReport.spec.ts b/apps/meteor/app/statistics/server/functions/sendUsageReport.spec.ts index f2fd40e80557a..323f352618477 100644 --- a/apps/meteor/app/statistics/server/functions/sendUsageReport.spec.ts +++ b/apps/meteor/app/statistics/server/functions/sendUsageReport.spec.ts @@ -1,43 +1,61 @@ -import { expect } from 'chai'; -import { describe, it, beforeEach, afterEach } from 'mocha'; -import proxyquire from 'proxyquire'; -import sinon from 'sinon'; +import type { Logger } from '@rocket.chat/logger'; +import { Statistics } from '@rocket.chat/models'; +import { serverFetch } from '@rocket.chat/server-fetch'; -const sandbox = sinon.createSandbox(); +import { statistics } from '..'; +import { sendUsageReport } from './sendUsageReport'; -const mocks = { +jest.mock('@rocket.chat/models', () => ({ Statistics: { - findLast: sandbox.stub(), - updateOne: sandbox.stub(), + findLast: jest.fn(), + updateOne: jest.fn(), }, +})); + +jest.mock('@rocket.chat/server-fetch', () => ({ + serverFetch: jest.fn(), +})); + +jest.mock('..', () => ({ statistics: { - save: sandbox.stub(), + save: jest.fn(), }, - serverFetch: sandbox.stub(), - getWorkspaceAccessToken: sandbox.stub().resolves('workspace-token'), +})); + +jest.mock('../../../cloud/server', () => ({ + getWorkspaceAccessToken: jest.fn().mockResolvedValue('workspace-token'), +})); + +jest.mock('meteor/meteor', () => ({ Meteor: { - absoluteUrl: sandbox.stub().returns('http://localhost:3000/'), - }, - logger: { - error: sandbox.stub(), + absoluteUrl: jest.fn().mockReturnValue('http://localhost:3000/'), }, +})); + +jest.mock('../../../utils/rocketchat.info', () => ({ Info: { version: '3.0.1', }, -}; - -const { sendUsageReport } = proxyquire.noCallThru().load('./sendUsageReport', { - '@rocket.chat/models': { Statistics: mocks.Statistics }, - '@rocket.chat/server-fetch': { serverFetch: mocks.serverFetch }, - '..': { statistics: mocks.statistics }, - '../../../cloud/server': { getWorkspaceAccessToken: mocks.getWorkspaceAccessToken }, - 'meteor/meteor': { Meteor: mocks.Meteor }, - '../../../utils/rocketchat.info': { Info: mocks.Info }, -}); +})); + +require('@rocket.chat/models'); +require('@rocket.chat/server-fetch'); +require('..'); +require('../../../cloud/server'); +require('meteor/meteor'); +require('../../../utils/rocketchat.info'); + +const mockFindLast = Statistics.findLast as jest.Mock; +const mockSave = statistics.save as jest.Mock; +const mockServerFetch = serverFetch as jest.Mock; describe('sendUsageReport', () => { + const mockLogger = { + error: jest.fn(), + } as unknown as Logger; + beforeEach(() => { - sandbox.resetHistory(); + jest.clearAllMocks(); }); afterEach(() => { @@ -47,59 +65,59 @@ describe('sendUsageReport', () => { it('should save statistics locally and not send to collector when RC_DISABLE_STATISTICS_REPORTING is true', async () => { process.env.RC_DISABLE_STATISTICS_REPORTING = 'true'; - const result = await sendUsageReport(mocks.logger); + const result = await sendUsageReport(mockLogger); - expect(mocks.statistics.save.called).to.be.true; - expect(mocks.serverFetch.called).to.be.false; - expect(result).to.be.undefined; + expect(mockSave).toHaveBeenCalled(); + expect(mockServerFetch).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); }); it('should save statistics locally and send to collector when RC_DISABLE_STATISTICS_REPORTING is false', async () => { process.env.RC_DISABLE_STATISTICS_REPORTING = 'false'; - const result = await sendUsageReport(mocks.logger); + const result = await sendUsageReport(mockLogger); - expect(mocks.statistics.save.called).to.be.true; - expect(mocks.serverFetch.calledOnce).to.be.true; - expect(mocks.serverFetch.calledWith('https://collector.rocket.chat/', sinon.match({ method: 'POST' }))).to.be.true; - expect(result).to.be.undefined; + expect(mockSave).toHaveBeenCalled(); + expect(mockServerFetch).toHaveBeenCalledTimes(1); + expect(mockServerFetch).toHaveBeenCalledWith('https://collector.rocket.chat/', expect.objectContaining({ method: 'POST' })); + expect(result).toBeUndefined(); }); it('should generate new statistics when version changes', async () => { - mocks.Statistics.findLast.resolves({ + mockFindLast.mockResolvedValue({ _id: 'stats-id', version: '2.9.0', createdAt: new Date(), }); - mocks.statistics.save.resolves({ _id: 'new-stats-id' }); + mockSave.mockResolvedValue({ _id: 'new-stats-id' }); - const result = await sendUsageReport(mocks.logger); + const result = await sendUsageReport(mockLogger); - expect(mocks.statistics.save.calledOnce).to.be.true; - expect(result).to.be.undefined; + expect(mockSave).toHaveBeenCalledTimes(1); + expect(result).toBeUndefined(); }); it('should NOT generate new statistics if last version equals current version', async () => { - mocks.Statistics.findLast.resolves({ + mockFindLast.mockResolvedValue({ _id: 'stats-id', version: '3.0.1', createdAt: new Date(), statsToken: 'token', }); - const result = await sendUsageReport(mocks.logger); + const result = await sendUsageReport(mockLogger); - expect(mocks.statistics.save.called).to.be.false; - expect(result).to.equal('token'); + expect(mockSave).not.toHaveBeenCalled(); + expect(result).toBe('token'); }); it('should generate new statistics when no previous stats exist', async () => { - mocks.Statistics.findLast.resolves(undefined); - mocks.statistics.save.resolves({ _id: 'new-stats-id' }); + mockFindLast.mockResolvedValue(undefined); + mockSave.mockResolvedValue({ _id: 'new-stats-id' }); - await sendUsageReport(mocks.logger); + await sendUsageReport(mockLogger); - expect(mocks.statistics.save.calledOnce).to.be.true; + expect(mockSave).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 4eda7577e1927..be02df5ca7476 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -40,7 +40,7 @@ export default { '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', '/app/*/lib/**/*.spec.ts', - '/app/statistics/server/lib/*.spec.ts', + '/app/statistics/**/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', '/server/services/import/**/*.spec.ts', '/server/settings/lib/**.spec.ts', From de36dc8f49cf2cee20300385bdca3135d25aaa2d Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 03:44:40 -0300 Subject: [PATCH 62/80] refactor: migrate unit test `apps/meteor/tests/unit/app/custom-oauth/server/transform_helpers.tests.js` --- .../server/transform_helpers.spec.ts} | 46 ++++++++----------- .../custom-oauth/server/transform_helpers.ts | 20 +++++++- apps/meteor/jest.config.ts | 1 + 3 files changed, 38 insertions(+), 29 deletions(-) rename apps/meteor/{tests/unit/app/custom-oauth/server/transform_helpers.tests.js => app/custom-oauth/server/transform_helpers.spec.ts} (68%) diff --git a/apps/meteor/tests/unit/app/custom-oauth/server/transform_helpers.tests.js b/apps/meteor/app/custom-oauth/server/transform_helpers.spec.ts similarity index 68% rename from apps/meteor/tests/unit/app/custom-oauth/server/transform_helpers.tests.js rename to apps/meteor/app/custom-oauth/server/transform_helpers.spec.ts index 17455dec833fd..143916008cb26 100644 --- a/apps/meteor/tests/unit/app/custom-oauth/server/transform_helpers.tests.js +++ b/apps/meteor/app/custom-oauth/server/transform_helpers.spec.ts @@ -1,12 +1,4 @@ -import { expect } from 'chai'; - -import { - normalizers, - fromTemplate, - renameInvalidProperties, - getNestedValue, - getRegexpMatch, -} from '../../../../../app/custom-oauth/server/transform_helpers'; +import { normalizers, fromTemplate, renameInvalidProperties, getNestedValue, getRegexpMatch } from './transform_helpers'; const data = { 'id': '123456', @@ -36,28 +28,28 @@ describe('fromTemplate', () => { const template = '{{/^foo@bar.(.+)/::email}}'; const expected = 'com'; const result = fromTemplate(template, normalizedData); - expect(result).to.equal(expected); + expect(result).toBe(expected); }); it('returns match from regexp on nested properties', () => { const template = '{{/^ba(.+)/::nested.value}}'; const expected = 'z'; const result = fromTemplate(template, normalizedData); - expect(result).to.equal(expected); + expect(result).toBe(expected); }); it('returns value from nested prop with plain syntax', () => { const template = 'nested.value'; const expected = normalizedData.nested.value; const result = fromTemplate(template, normalizedData); - expect(result).to.equal(expected); + expect(result).toBe(expected); }); it('returns value from nested prop with template syntax', () => { const template = '{{nested.value}}'; const expected = normalizedData.nested.value; const result = fromTemplate(template, normalizedData); - expect(result).to.equal(expected); + expect(result).toBe(expected); }); it('returns composed value from nested prop with template syntax', () => { @@ -65,31 +57,31 @@ describe('fromTemplate', () => { const expected = `${normalizedData.name}.${normalizedData.nested.value}`; const result = fromTemplate(template, normalizedData); - expect(result).to.equal(expected); + expect(result).toBe(expected); }); it('returns composed string from multiple template chunks with static parts', () => { const template = 'composed-{{name}}-at-{{nested.value}}-dot-{{/^foo@bar.(.+)/::email}}-from-template'; const expected = 'composed-foo-at-baz-dot-com-from-template'; const result = fromTemplate(template, normalizedData); - expect(result).to.equal(expected); + expect(result).toBe(expected); }); }); describe('getRegexpMatch', () => { it('returns nested value when formula is not in the regex::field form', () => { const formula = 'nested.value'; - expect(getRegexpMatch(formula, data)).to.equal(data.nested.value); + expect(getRegexpMatch(formula, data)).toBe(data.nested.value); }); it("returns undefined when regex doesn't match", () => { const formula = '/^foo@baz(.+)/::email'; - expect(getRegexpMatch(formula, data)).to.be.undefined; + expect(getRegexpMatch(formula, data)).toBeUndefined(); }); it("throws when regex isn't valid", () => { const formula = '/+/::email'; - expect(() => getRegexpMatch(formula, data)).to.throw(); + expect(() => getRegexpMatch(formula, data)).toThrow(); }); }); @@ -97,25 +89,25 @@ describe('renameInvalidProperties', () => { it('replaces . chars in field names with _', () => { const result = renameInvalidProperties(data); - expect(result['invalid.property']).to.be.undefined; - expect(result.invalid_property).to.equal(data['invalid.property']); + expect((result as Record)['invalid.property']).toBeUndefined(); + expect(result.invalid_property).toBe(data['invalid.property']); - expect(result.nested['invalid.property']).to.be.undefined; - expect(result.nested.invalid_property).to.equal(data.nested['invalid.property']); + expect((result.nested as Record)['another.invalid.prop']).toBeUndefined(); + expect(result.nested.another_invalid_prop).toBe(data.nested['another.invalid.prop']); - result.list.forEach((item, idx) => { - expect(item['invalid.property']).to.be.undefined; - expect(item.invalid_property).to.equal(data.list[idx]['invalid.property']); + result.list.forEach((item: any, idx: number) => { + expect(item['invalid.property']).toBeUndefined(); + expect(item.invalid_property).toBe(data.list[idx]['invalid.property']); }); }); }); describe('getNestedValue', () => { it("returns undefined when nested value doesn't exist", () => { - expect(getNestedValue('nested.does.not.exist', data)).to.be.undefined; + expect(getNestedValue('nested.does.not.exist', data)).toBeUndefined(); }); it('returns nested object property', () => { - expect(getNestedValue('nested.value', data)).to.equal(data.nested.value); + expect(getNestedValue('nested.value', data)).toBe(data.nested.value); }); }); diff --git a/apps/meteor/app/custom-oauth/server/transform_helpers.ts b/apps/meteor/app/custom-oauth/server/transform_helpers.ts index 1238538a8b74d..4d431bdbb4860 100644 --- a/apps/meteor/app/custom-oauth/server/transform_helpers.ts +++ b/apps/meteor/app/custom-oauth/server/transform_helpers.ts @@ -104,7 +104,23 @@ export const normalizers: Record Identity | void }; const IDENTITY_PROPNAME_FILTER = /(\.)/g; -export const renameInvalidProperties = (input: any): any => { + +type Replace = S extends `${infer Prefix}${From}${infer Suffix}` + ? `${Prefix}${To}${Replace}` + : S; +type TransformObjectKeys = { + [K in keyof T as K extends string ? (K extends `${string}.${string}` ? Replace : K) : K]: T[K] extends any[] + ? TransformArrayItems + : T[K] extends Record + ? TransformObjectKeys + : T[K]; +}; +type TransformArrayItems = T extends Array ? (U extends Record ? Array> : U[]) : T; + +export function renameInvalidProperties(input: T): T; +export function renameInvalidProperties(input: T): T; +export function renameInvalidProperties>(input: T): TransformObjectKeys; +export function renameInvalidProperties(input: any): any { if (Array.isArray(input)) { return input.map(renameInvalidProperties); } @@ -119,7 +135,7 @@ export const renameInvalidProperties = (input: any): any => { }), {}, ); -}; +} export const getNestedValue = (propertyPath: string, source: any): any => propertyPath.split('.').reduce((prev, curr) => (prev ? prev[curr] : undefined), source); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index be02df5ca7476..9fd1a50d6b9a3 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -39,6 +39,7 @@ export default { '/ee/app/license/server/**/*.spec.ts', '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', + '/app/custom-oauth/server/**/*.spec.ts', '/app/*/lib/**/*.spec.ts', '/app/statistics/**/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', From 962e2e7c9d918756258f65dcf5521b3d36490423 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 04:20:29 -0300 Subject: [PATCH 63/80] refactor: migrate unit test `apps/meteor/tests/unit/app/highlight-words/helper.tests.js` --- apps/meteor/.mocharc.js | 2 ++ .../highlight-words/client/helper.spec.ts} | 12 +++++------- apps/meteor/jest.config.ts | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) rename apps/meteor/{tests/unit/app/highlight-words/helper.tests.js => app/highlight-words/client/helper.spec.ts} (77%) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index f23763955406f..3ce18abe6c583 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -29,5 +29,7 @@ module.exports = { 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', 'app/livechat/server/lib/**/*.spec.ts', + '!app/custom-oauth/server/**/*.spec.ts', + '!app/highlight-words/client/**/*.spec.ts', ], }; diff --git a/apps/meteor/tests/unit/app/highlight-words/helper.tests.js b/apps/meteor/app/highlight-words/client/helper.spec.ts similarity index 77% rename from apps/meteor/tests/unit/app/highlight-words/helper.tests.js rename to apps/meteor/app/highlight-words/client/helper.spec.ts index 735ad51fe21c0..a3d5412bcc768 100644 --- a/apps/meteor/tests/unit/app/highlight-words/helper.tests.js +++ b/apps/meteor/app/highlight-words/client/helper.spec.ts @@ -1,6 +1,4 @@ -import { expect } from 'chai'; - -import { highlightWords, getRegexHighlight, getRegexHighlightUrl } from '../../../../app/highlight-words/client/helper'; +import { highlightWords, getRegexHighlight, getRegexHighlightUrl } from './helper'; describe('helper', () => { describe('highlightWords', () => { @@ -14,7 +12,7 @@ describe('helper', () => { })), ); - expect(res).to.be.equal('here is some word'); + expect(res).toBe('here is some word'); }); describe('handles links', () => { @@ -28,7 +26,7 @@ describe('helper', () => { })), ); - expect(res).to.be.equal('here we go https://somedomain.com/here-some.word/pulls more words after'); + expect(res).toBe('here we go https://somedomain.com/here-some.word/pulls more words after'); }); it('not highlighting two links', () => { @@ -42,7 +40,7 @@ describe('helper', () => { })), ); - expect(res).to.be.equal(msg); + expect(res).toBe(msg); }); it('not highlighting link but keep words on message highlighted', () => { @@ -55,7 +53,7 @@ describe('helper', () => { })), ); - expect(res).to.be.equal('here we go https://somedomain.com/here-some.foo/pulls more foo after'); + expect(res).toBe('here we go https://somedomain.com/here-some.foo/pulls more foo after'); }); }); }); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index 9fd1a50d6b9a3..b41f829cb02a2 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -12,6 +12,7 @@ export default { testMatch: [ '/client/**/**.spec.[jt]s?(x)', '/ee/client/**/**.spec.[jt]s?(x)', + '/app/highlight-words/client/**/*.spec.ts', '/app/ui-message/client/**/**.spec.[jt]s?(x)', '/tests/unit/client/views/**/*.spec.{ts,tsx}', '/tests/unit/client/providers/**/*.spec.{ts,tsx}', @@ -40,8 +41,8 @@ export default { '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', '/app/custom-oauth/server/**/*.spec.ts', - '/app/*/lib/**/*.spec.ts', '/app/statistics/**/*.spec.ts', + '/app/*/lib/**/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', '/server/services/import/**/*.spec.ts', '/server/settings/lib/**.spec.ts', From 5a0957b515796fa69c958714614e7ea7b2b46ba5 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 04:43:52 -0300 Subject: [PATCH 64/80] refactor: migrate unit test `apps/meteor/tests/unit/app/mentions/server.tests.js` --- apps/meteor/.mocharc.js | 1 + .../mentions/server/Mentions.spec.ts} | 118 +++++++++--------- apps/meteor/jest.config.ts | 1 + 3 files changed, 64 insertions(+), 56 deletions(-) rename apps/meteor/{tests/unit/app/mentions/server.tests.js => app/mentions/server/Mentions.spec.ts} (75%) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 3ce18abe6c583..187fd15f1778c 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -31,5 +31,6 @@ module.exports = { 'app/livechat/server/lib/**/*.spec.ts', '!app/custom-oauth/server/**/*.spec.ts', '!app/highlight-words/client/**/*.spec.ts', + '!app/mentions/server/**/*.spec.ts', ], }; diff --git a/apps/meteor/tests/unit/app/mentions/server.tests.js b/apps/meteor/app/mentions/server/Mentions.spec.ts similarity index 75% rename from apps/meteor/tests/unit/app/mentions/server.tests.js rename to apps/meteor/app/mentions/server/Mentions.spec.ts index 04064de7c3cab..33c7e11fad315 100644 --- a/apps/meteor/tests/unit/app/mentions/server.tests.js +++ b/apps/meteor/app/mentions/server/Mentions.spec.ts @@ -1,8 +1,6 @@ -import { expect } from 'chai'; +import { MentionsServer } from './Mentions'; -import { MentionsServer } from '../../../../app/mentions/server/Mentions'; - -let mention; +let mention: MentionsServer; beforeEach(() => { mention = new MentionsServer({ @@ -11,25 +9,29 @@ beforeEach(() => { getUsers: async (usernames) => [ { - _id: 1, + type: 'user' as const, + _id: '1', username: 'rocket.cat', }, { - _id: 2, + type: 'user' as const, + _id: '2', username: 'jon', }, ].filter((user) => usernames.includes(user.username)), // Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch(); - getChannels(channels) { - return [ + getChannels: async (channels) => + [ { - _id: 1, + _id: '1', name: 'general', }, - ].filter((channel) => channels.includes(channel.name)); - // return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); + ].filter((channel) => channels.includes(channel.name)) as any, + // return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c' }, { fields: {_id: 1, name: 1 }}).fetch(); + getUser: async (userId) => ({ _id: userId, language: 'en' }) as any, + getTotalChannelMembers: async (/* rid*/) => 2, + onMaxRoomMembersExceeded: async () => { + /* do nothing */ }, - getUser: (userId) => ({ _id: userId, language: 'en' }), - getTotalChannelMembers: (/* rid*/) => 2, }); }); @@ -37,27 +39,27 @@ describe('Mention Server', () => { describe('getUsersByMentions', () => { describe('for @all but the number of users is greater than messageMaxAll', () => { beforeEach(() => { - mention.getTotalChannelMembers = () => 5; + mention.getTotalChannelMembers = async () => 5; }); it('should return nothing', async () => { const message = { msg: '@all', - }; - const expected = []; + } as any; + const expected: any[] = []; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); }); describe('for one user', () => { beforeEach(() => { - mention.getChannel = () => ({ + (mention as any).getChannel = () => ({ usernames: [ { - _id: 1, + _id: '1', username: 'rocket.cat', }, { - _id: 2, + _id: '2', username: 'jon', }, ], @@ -67,7 +69,7 @@ describe('Mention Server', () => { it('should return "all"', async () => { const message = { msg: '@all', - }; + } as any; const expected = [ { _id: 'all', @@ -75,12 +77,12 @@ describe('Mention Server', () => { }, ]; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); it('should return "here"', async () => { const message = { msg: '@here', - }; + } as any; const expected = [ { _id: 'here', @@ -88,27 +90,28 @@ describe('Mention Server', () => { }, ]; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); it('should return "rocket.cat"', async () => { const message = { msg: '@rocket.cat', - }; + } as any; const expected = [ { - _id: 1, + type: 'user', + _id: '1', username: 'rocket.cat', }, ]; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); }); describe('for two user', () => { it('should return "all and here"', async () => { const message = { msg: '@all @here', - }; + } as any; const expected = [ { _id: 'all', @@ -120,46 +123,49 @@ describe('Mention Server', () => { }, ]; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); it('should return "here and rocket.cat"', async () => { const message = { msg: '@here @rocket.cat', - }; + } as any; const expected = [ { _id: 'here', username: 'here', }, { - _id: 1, + type: 'user', + _id: '1', username: 'rocket.cat', }, ]; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); it('should return "here, rocket.cat, jon"', async () => { const message = { msg: '@here @rocket.cat @jon', - }; + } as any; const expected = [ { _id: 'here', username: 'here', }, { - _id: 1, + type: 'user', + _id: '1', username: 'rocket.cat', }, { - _id: 2, + type: 'user', + _id: '2', username: 'jon', }, ]; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); }); @@ -167,10 +173,10 @@ describe('Mention Server', () => { it('should return "nothing"', async () => { const message = { msg: '@unknow', - }; - const expected = []; + } as any; + const expected: any[] = []; const result = await mention.getUsersByMentions(message); - expect(expected).to.be.deep.equal(result); + expect(result).toEqual(expected); }); }); }); @@ -178,56 +184,56 @@ describe('Mention Server', () => { it('should return the channel "general"', async () => { const message = { msg: '#general', - }; + } as any; const expected = [ { - _id: 1, + _id: '1', name: 'general', }, ]; const result = await mention.getChannelbyMentions(message); - expect(result).to.be.deep.equal(expected); + expect(result).toEqual(expected); }); it('should return nothing"', async () => { const message = { msg: '#unknow', - }; - const expected = []; + } as any; + const expected: any[] = []; const result = await mention.getChannelbyMentions(message); - expect(result).to.be.deep.equal(expected); + expect(result).toEqual(expected); }); }); describe('execute', () => { it('should return the channel "general"', async () => { const message = { msg: '#general', - }; + } as any; const expected = [ { - _id: 1, + _id: '1', name: 'general', }, ]; const result = await mention.getChannelbyMentions(message); - expect(result).to.be.deep.equal(expected); + expect(result).toEqual(expected); }); it('should return nothing"', async () => { const message = { msg: '#unknow', - }; + } as any; const expected = { msg: '#unknow', mentions: [], channels: [], }; const result = await mention.execute(message); - expect(result).to.be.deep.equal(expected); + expect(result).toEqual(expected); }); }); describe('getUserMentions', () => { describe('for message with only an md link', () => { - const result = []; + const result: string[] = []; [ '[@rocket.cat](https://rocket.chat)', '[@rocket.cat](https://rocket.chat) hello', @@ -235,7 +241,7 @@ describe('Mention Server', () => { '[test](https://rocket.chat)', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mention.getUserMentions(text)); + expect(mention.getUserMentions(text)).toEqual(result); }); }); }); @@ -249,7 +255,7 @@ describe('Mention Server', () => { '@sauron please work on [user@password](https://rocket.chat) hello', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mention.getUserMentions(text)); + expect(mention.getUserMentions(text)).toEqual(result); }); }); }); @@ -257,7 +263,7 @@ describe('Mention Server', () => { describe('getChannelMentions', () => { describe('for message with md link', () => { - const result = []; + const result: string[] = []; [ '[#general](https://rocket.chat)', '[#general](https://rocket.chat) hello', @@ -265,7 +271,7 @@ describe('Mention Server', () => { '[test #general #other](https://rocket.chat)', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mention.getChannelMentions(text)); + expect(mention.getChannelMentions(text)).toEqual(result); }); }); }); @@ -279,7 +285,7 @@ describe('Mention Server', () => { '#somechannel join [#general on #other](https://rocket.chat)', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mention.getChannelMentions(text)); + expect(mention.getChannelMentions(text)).toEqual(result); }); }); }); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index b41f829cb02a2..d0f25f8c1ea94 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -41,6 +41,7 @@ export default { '/ee/server/patches/**/*.spec.ts', '/app/cloud/server/functions/supportedVersionsToken/**.spec.ts', '/app/custom-oauth/server/**/*.spec.ts', + '/app/mentions/server/*.spec.ts', '/app/statistics/**/*.spec.ts', '/app/*/lib/**/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', From b1e461b7cee6fb1b36ecc48638fafacac45762e4 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 05:24:12 -0300 Subject: [PATCH 65/80] refactor: migrate unit test `apps/meteor/tests/unit/app/mentions/client.tests.js` --- apps/meteor/.mocharc.js | 3 +- .../mentions/lib/MentionsParser.spec.ts} | 126 +++++++++--------- 2 files changed, 63 insertions(+), 66 deletions(-) rename apps/meteor/{tests/unit/app/mentions/client.tests.js => app/mentions/lib/MentionsParser.spec.ts} (63%) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 187fd15f1778c..d049272bd8059 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -29,8 +29,7 @@ module.exports = { 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', 'app/livechat/server/lib/**/*.spec.ts', - '!app/custom-oauth/server/**/*.spec.ts', - '!app/highlight-words/client/**/*.spec.ts', '!app/mentions/server/**/*.spec.ts', + '!app/mentions/lib/**/*.spec.ts', ], }; diff --git a/apps/meteor/tests/unit/app/mentions/client.tests.js b/apps/meteor/app/mentions/lib/MentionsParser.spec.ts similarity index 63% rename from apps/meteor/tests/unit/app/mentions/client.tests.js rename to apps/meteor/app/mentions/lib/MentionsParser.spec.ts index 70ac7b9432019..717ff22ce587a 100644 --- a/apps/meteor/tests/unit/app/mentions/client.tests.js +++ b/apps/meteor/app/mentions/lib/MentionsParser.spec.ts @@ -1,8 +1,8 @@ -import { expect } from 'chai'; +import type { IMessage } from '@rocket.chat/core-typings'; -import { MentionsParser } from '../../../../app/mentions/lib/MentionsParser'; +import { MentionsParser } from './MentionsParser'; -let mentionsParser; +let mentionsParser: MentionsParser; beforeEach(() => { mentionsParser = new MentionsParser({ pattern: () => '[0-9a-zA-Z-_.]+', @@ -13,10 +13,10 @@ beforeEach(() => { describe('Mention', () => { describe('getUserMentions', () => { describe('for simple text, no mentions', () => { - const result = []; + const result: string[] = []; ['#rocket.cat', 'hello rocket.cat how are you?'].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text)); + expect(result).toEqual(mentionsParser.getUserMentions(text)); }); }); }); @@ -33,20 +33,20 @@ describe('Mention', () => { 'hello @rocket.cat how are you?', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text)); + expect(result).toEqual(mentionsParser.getUserMentions(text)); }); }); it.skip('should return without the "." from "@rocket.cat."', () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat.')); + expect(result).toEqual(mentionsParser.getUserMentions('@rocket.cat.')); }); it.skip('should return without the "_" from "@rocket.cat_"', () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat_')); + expect(result).toEqual(mentionsParser.getUserMentions('@rocket.cat_')); }); it.skip('should return without the "-" from "@rocket.cat-"', () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions('@rocket.cat-')); + expect(result).toEqual(mentionsParser.getUserMentions('@rocket.cat-')); }); }); @@ -60,7 +60,7 @@ describe('Mention', () => { 'hello @rocket.cat and @all how are you?', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions(text)); + expect(result).toEqual(mentionsParser.getUserMentions(text)); }); }); }); @@ -68,10 +68,10 @@ describe('Mention', () => { describe('getChannelMentions', () => { describe('for simple text, no mentions', () => { - const result = []; + const result: string[] = []; ['@rocket.cat', 'hello rocket.cat how are you?'].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); + expect(result).toEqual(mentionsParser.getChannelMentions(text)); }); }); }); @@ -80,20 +80,20 @@ describe('Mention', () => { const result = ['#general']; ['#general', ' #general ', 'hello #general', '#general, hello', 'hello #general, how are you?'].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); + expect(result).toEqual(mentionsParser.getChannelMentions(text)); }); }); it.skip('should return without the "." from "#general."', () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general.')); + expect(result).toEqual(mentionsParser.getUserMentions('#general.')); }); it.skip('should return without the "_" from "#general_"', () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general_')); + expect(result).toEqual(mentionsParser.getUserMentions('#general_')); }); it.skip('should return without the "-" from "#general."', () => { - expect(result).to.be.deep.equal(mentionsParser.getUserMentions('#general-')); + expect(result).toEqual(mentionsParser.getUserMentions('#general-')); }); }); @@ -107,16 +107,16 @@ describe('Mention', () => { 'hello #general #other, how are you?', ].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); + expect(result).toEqual(mentionsParser.getChannelMentions(text)); }); }); }); describe('for url with fragments', () => { - const result = []; + const result: string[] = []; ['http://localhost/#general'].forEach((text) => { it(`should return nothing from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); + expect(result).toEqual(mentionsParser.getChannelMentions(text)); }); }); }); @@ -125,14 +125,14 @@ describe('Mention', () => { const result = ['#general']; ['http://localhost/#general #general'].forEach((text) => { it(`should return "${JSON.stringify(result)}" from "${text}"`, () => { - expect(result).to.be.deep.equal(mentionsParser.getChannelMentions(text)); + expect(result).toEqual(mentionsParser.getChannelMentions(text)); }); }); }); }); }); -const message = { +const message: IMessage = { mentions: [ { username: 'rocket.cat', name: 'Rocket.Cat' }, { username: 'admin', name: 'Admin' }, @@ -143,35 +143,35 @@ const message = { { name: 'general', _id: '42' }, { name: 'rocket.cat', _id: '169' }, ], -}; +} as any; describe('replace methods', () => { describe('replaceUsers', () => { it('should render for @all', () => { const result = mentionsParser.replaceUsers('@all', message, 'me'); - expect(result).to.be.equal('all'); + expect(result).toBe('all'); }); const str2 = 'rocket.cat'; it(`should render for "@${str2}"`, () => { const result = mentionsParser.replaceUsers(`@${str2}`, message, 'me'); - expect(result).to.be.equal(`${str2}`); + expect(result).toBe(`${str2}`); }); it(`should render for "hello ${str2}"`, () => { const result = mentionsParser.replaceUsers(`hello @${str2}`, message, 'me'); - expect(result).to.be.equal(`hello ${str2}`); + expect(result).toBe(`hello ${str2}`); }); it('should render for unknow/private user "hello @unknow"', () => { const result = mentionsParser.replaceUsers('hello @unknow', message, 'me'); - expect(result).to.be.equal('hello @unknow'); + expect(result).toBe('hello @unknow'); }); it('should render for me', () => { const result = mentionsParser.replaceUsers('hello @me', message, 'me'); - expect(result).to.be.equal('hello me'); + expect(result).toBe('hello me'); }); }); @@ -182,7 +182,7 @@ describe('replace methods', () => { it('should render for @all', () => { const result = mentionsParser.replaceUsers('@all', message, 'me'); - expect(result).to.be.equal('all'); + expect(result).toBe('all'); }); const str2 = 'rocket.cat'; @@ -190,14 +190,12 @@ describe('replace methods', () => { it(`should render for "@${str2}"`, () => { const result = mentionsParser.replaceUsers(`@${str2}`, message, 'me'); - expect(result).to.be.equal(`${str2Name}`); + expect(result).toBe(`${str2Name}`); }); it(`should render for "hello @${str2}"`, () => { const result = mentionsParser.replaceUsers(`hello @${str2}`, message, 'me'); - expect(result).to.be.equal( - `hello ${str2Name}`, - ); + expect(result).toBe(`hello ${str2Name}`); }); const specialchars = 'specialchars'; @@ -205,78 +203,78 @@ describe('replace methods', () => { it(`should escape special characters in "hello @${specialchars}"`, () => { const result = mentionsParser.replaceUsers(`hello @${specialchars}`, message, 'me'); - expect(result).to.be.equal( + expect(result).toBe( `hello ${specialcharsName}`, ); }); it(`should render for "hello
@${str2}
"`, () => { const result = mentionsParser.replaceUsers(`hello
@${str2}
`, message, 'me'); - expect(result).to.be.equal( + expect(result).toBe( `hello
${str2Name}
`, ); }); it('should render for unknow/private user "hello @unknow"', () => { const result = mentionsParser.replaceUsers('hello @unknow', message, 'me'); - expect(result).to.be.equal('hello @unknow'); + expect(result).toBe('hello @unknow'); }); it('should render for me', () => { const result = mentionsParser.replaceUsers('hello @me', message, 'me'); - expect(result).to.be.equal('hello Me'); + expect(result).toBe('hello Me'); }); }); describe('replaceChannels', () => { it('should render for #general', () => { const result = mentionsParser.replaceChannels('#general', message); - expect('<).to.be.equal(class="mention-link mention-link--room" data-channel="42">#general', result); + expect(result).toBe('#general'); }); const str2 = '#rocket.cat'; it(`should render for ${str2}`, () => { const result = mentionsParser.replaceChannels(str2, message); - expect(result).to.be.equal(`${str2}`); + expect(result).toBe(`${str2}`); }); it(`should render for "hello ${str2}"`, () => { const result = mentionsParser.replaceChannels(`hello ${str2}`, message); - expect(result).to.be.equal(`hello ${str2}`); + expect(result).toBe(`hello ${str2}`); }); it('should render for unknow/private channel "hello #unknow"', () => { const result = mentionsParser.replaceChannels('hello #unknow', message); - expect(result).to.be.equal('hello #unknow'); + expect(result).toBe('hello #unknow'); }); }); describe('parse all', () => { it('should render for #general', () => { - message.html = '#general'; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal('#general'); + const testMessage = { ...message, html: '#general' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe('#general'); }); it('should render for "#general and @rocket.cat', () => { - message.html = '#general and @rocket.cat'; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal( + const testMessage = { ...message, html: '#general and @rocket.cat' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe( '#general and rocket.cat', ); }); it('should render for "', () => { - message.html = ''; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal(''); + const testMessage = { ...message, html: '' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe(''); }); it('should render for "simple text', () => { - message.html = 'simple text'; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal('simple text'); + const testMessage = { ...message, html: 'simple text' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe('simple text'); }); }); @@ -286,29 +284,29 @@ describe('replace methods', () => { }); it('should render for #general', () => { - message.html = '#general'; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal('#general'); + const testMessage = { ...message, html: '#general' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe('#general'); }); it('should render for "#general and @rocket.cat', () => { - message.html = '#general and @rocket.cat'; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal( + const testMessage = { ...message, html: '#general and @rocket.cat' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe( '#general and Rocket.Cat', ); }); it('should render for "', () => { - message.html = ''; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal(''); + const testMessage = { ...message, html: '' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe(''); }); it('should render for "simple text', () => { - message.html = 'simple text'; - const result = mentionsParser.parse(message, 'me'); - expect(result.html).to.be.equal('simple text'); + const testMessage = { ...message, html: 'simple text' }; + const result = mentionsParser.parse(testMessage); + expect(result.html).toBe('simple text'); }); }); }); From fb02e6e461508c2e29442567344caad0fa3c2a08 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 11:11:43 -0300 Subject: [PATCH 66/80] refactor: migrate unit test `apps/meteor/tests/unit/app/utils/lib/getURL.tests.js` --- apps/meteor/.mocharc.js | 1 + .../utils/lib/getURL.spec.ts} | 54 +++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) rename apps/meteor/{tests/unit/app/utils/lib/getURL.tests.js => app/utils/lib/getURL.spec.ts} (74%) diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index d049272bd8059..ab26d7c50e9d2 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -31,5 +31,6 @@ module.exports = { 'app/livechat/server/lib/**/*.spec.ts', '!app/mentions/server/**/*.spec.ts', '!app/mentions/lib/**/*.spec.ts', + '!app/utils/lib/**/*.spec.ts', ], }; diff --git a/apps/meteor/tests/unit/app/utils/lib/getURL.tests.js b/apps/meteor/app/utils/lib/getURL.spec.ts similarity index 74% rename from apps/meteor/tests/unit/app/utils/lib/getURL.tests.js rename to apps/meteor/app/utils/lib/getURL.spec.ts index 8a714a143e65a..9f6f4a44614f4 100644 --- a/apps/meteor/tests/unit/app/utils/lib/getURL.tests.js +++ b/apps/meteor/app/utils/lib/getURL.spec.ts @@ -1,20 +1,16 @@ -import { expect } from 'chai'; -import proxyquire from 'proxyquire'; - -import { ltrim, rtrim } from '../../../../../lib/utils/stringUtils'; - -const { _getURL } = proxyquire.noCallThru().load('../../../../../app/utils/lib/getURL', { - 'meteor/meteor': { - 'Meteor': { - absoluteUrl() { - return 'http://localhost:3000/'; - }, +import { _getURL } from './getURL'; +import { ltrim, rtrim } from '../../../lib/utils/stringUtils'; + +// Mock Meteor +jest.mock('meteor/meteor', () => ({ + Meteor: { + absoluteUrl() { + return 'http://localhost:3000/'; }, - '@global': true, }, -}); +})); -const testPaths = (o, _processPath) => { +const testPaths = (o: any, _processPath: (path: string) => string) => { let processPath = _processPath; if (o._root_url_path_prefix !== '') { processPath = (path) => _processPath(o._root_url_path_prefix + path); @@ -22,20 +18,21 @@ const testPaths = (o, _processPath) => { const cloudDeepLinkUrl = 'https://go.rocket.chat'; - expect(_getURL('', o, cloudDeepLinkUrl)).to.be.equal(processPath('')); - expect(_getURL('/', o, cloudDeepLinkUrl)).to.be.equal(processPath('')); - expect(_getURL('//', o, cloudDeepLinkUrl)).to.be.equal(processPath('')); - expect(_getURL('///', o, cloudDeepLinkUrl)).to.be.equal(processPath('')); - expect(_getURL('/channel', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel')); - expect(_getURL('/channel/', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel')); - expect(_getURL('/channel//', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel')); - expect(_getURL('/channel/123', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123')); - expect(_getURL('/channel/123/', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123')); - expect(_getURL('/channel/123?id=456&name=test', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123?id=456&name=test')); - expect(_getURL('/channel/123/?id=456&name=test', o, cloudDeepLinkUrl)).to.be.equal(processPath('/channel/123?id=456&name=test')); + expect(_getURL('', o, cloudDeepLinkUrl)).toBe(processPath('')); + expect(_getURL('/', o, cloudDeepLinkUrl)).toBe(processPath('')); + expect(_getURL('//', o, cloudDeepLinkUrl)).toBe(processPath('')); + expect(_getURL('///', o, cloudDeepLinkUrl)).toBe(processPath('')); + expect(_getURL('/channel', o, cloudDeepLinkUrl)).toBe(processPath('/channel')); + expect(_getURL('/channel/', o, cloudDeepLinkUrl)).toBe(processPath('/channel')); + expect(_getURL('/channel//', o, cloudDeepLinkUrl)).toBe(processPath('/channel')); + expect(_getURL('/channel/123', o, cloudDeepLinkUrl)).toBe(processPath('/channel/123')); + expect(_getURL('/channel/123/', o, cloudDeepLinkUrl)).toBe(processPath('/channel/123')); + expect(_getURL('/channel/123?id=456&name=test', o, cloudDeepLinkUrl)).toBe(processPath('/channel/123?id=456&name=test')); + expect(_getURL('/channel/123/?id=456&name=test', o, cloudDeepLinkUrl)).toBe(processPath('/channel/123?id=456&name=test')); }; -const getCloudUrl = (_site_url, path) => { +// eslint-disable-next-line @typescript-eslint/naming-convention +const getCloudUrl = (_site_url: string, path: string) => { path = ltrim(path, '/'); const url = `https://go.rocket.chat/?host=${encodeURIComponent(_site_url.replace(/https?:\/\//, ''))}&path=${encodeURIComponent(path)}`; if (_site_url.includes('http://')) { @@ -44,7 +41,8 @@ const getCloudUrl = (_site_url, path) => { return url; }; -const testCases = (options) => { +const testCases = (options: any) => { + // eslint-disable-next-line @typescript-eslint/naming-convention const _site_url = rtrim(options._site_url, '/'); if (!options.cloud) { @@ -116,7 +114,7 @@ const testCases = (options) => { } }; -const testCasesForOptions = (description, options) => { +const testCasesForOptions = (description: string, options: any) => { describe(description, () => { testCases({ ...options, cdn: false, full: false, cloud: false }); testCases({ ...options, cdn: true, full: false, cloud: false }); From 10669dd2631bc5884564ff7e8b521981f3da4816 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sat, 31 Jan 2026 23:38:36 -0300 Subject: [PATCH 67/80] refactor: migrate some mock modules to TypeScript --- .../{messages.data.js => messages.data.ts} | 0 .../server/mocks/models/BaseModel.mock.js | 5 -- .../server/mocks/models/BaseModel.mock.ts | 7 +++ .../{Messages.mock.js => Messages.mock.ts} | 8 ++- .../models/{Rooms.mock.js => Rooms.mock.ts} | 23 ++++++-- .../models/{Users.mock.js => Users.mock.ts} | 14 +++-- .../mocks/models/{index.js => index.ts} | 0 ...hestrator.mock.js => orchestrator.mock.ts} | 55 ++++++++++++------- .../tests/unit/app/apps/server/rooms.tests.ts | 31 ++++++----- 9 files changed, 91 insertions(+), 52 deletions(-) rename apps/meteor/tests/unit/app/apps/server/mocks/data/{messages.data.js => messages.data.ts} (100%) delete mode 100644 apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.js create mode 100644 apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.ts rename apps/meteor/tests/unit/app/apps/server/mocks/models/{Messages.mock.js => Messages.mock.ts} (82%) rename apps/meteor/tests/unit/app/apps/server/mocks/models/{Rooms.mock.js => Rooms.mock.ts} (89%) rename apps/meteor/tests/unit/app/apps/server/mocks/models/{Users.mock.js => Users.mock.ts} (72%) rename apps/meteor/tests/unit/app/apps/server/mocks/models/{index.js => index.ts} (100%) rename apps/meteor/tests/unit/app/apps/server/mocks/{orchestrator.mock.js => orchestrator.mock.ts} (56%) diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.js b/apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.ts similarity index 100% rename from apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.js rename to apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.ts diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.js deleted file mode 100644 index 920db0d95fdc9..0000000000000 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.js +++ /dev/null @@ -1,5 +0,0 @@ -export class BaseModelMock { - findOneById(id) { - return this.data[id]; - } -} diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.ts b/apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.ts new file mode 100644 index 0000000000000..22bcecbced825 --- /dev/null +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/BaseModel.mock.ts @@ -0,0 +1,7 @@ +export class BaseModelMock { + protected data: Record = {}; + + findOneById(id: string): T | null { + return this.data[id]; + } +} diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/Messages.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/Messages.mock.ts similarity index 82% rename from apps/meteor/tests/unit/app/apps/server/mocks/models/Messages.mock.js rename to apps/meteor/tests/unit/app/apps/server/mocks/models/Messages.mock.ts index e0e2880546fa8..d5cdfa702ce4d 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/Messages.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/Messages.mock.ts @@ -1,10 +1,12 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + import { BaseModelMock } from './BaseModel.mock'; -export class MessagesMock extends BaseModelMock { - data = { +export class MessagesMock extends BaseModelMock { + override data = { SimpleMessageMock: { _id: 'SimpleMessageMock', - t: 'uj', + t: 'uj' as const, rid: 'GENERAL', ts: new Date('2019-03-30T01:22:08.389Z'), msg: 'rocket.cat', diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.ts similarity index 89% rename from apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js rename to apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.ts index de59997324c67..fe958e477553f 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.ts @@ -1,11 +1,13 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + import { BaseModelMock } from './BaseModel.mock'; -export class RoomsMock extends BaseModelMock { - data = { +export class RoomsMock extends BaseModelMock { + override data = { GENERAL: { _id: 'GENERAL', ts: new Date('2019-03-27T20:51:36.808Z'), - t: 'c', + t: 'c' as const, name: 'general', usernames: [], msgs: 31, @@ -13,8 +15,8 @@ export class RoomsMock extends BaseModelMock { default: true, _updatedAt: new Date('2019-04-10T17:44:34.931Z'), lastMessage: { - _id: 1, - t: 'uj', + _id: 1 as unknown as string, // FIXME + t: 'uj' as const, rid: 'GENERAL', ts: new Date('2019-03-30T01:22:08.389Z'), msg: 'rocket.cat', @@ -26,6 +28,10 @@ export class RoomsMock extends BaseModelMock { _updatedAt: new Date('2019-03-30T01:22:08.412Z'), }, lm: new Date('2019-04-10T17:44:34.873Z'), + u: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, }, LivechatRoom: { @@ -34,7 +40,7 @@ export class RoomsMock extends BaseModelMock { usersCount: 1, lm: new Date('2019-04-07T23:45:25.407Z'), fname: 'Livechat Guest', - t: 'l', + t: 'l' as const, ts: new Date('2019-04-06T03:56:17.040Z'), v: { _id: 'yDLaWs5Rzf5mzQsmB', @@ -88,6 +94,11 @@ export class RoomsMock extends BaseModelMock { _id: 'rocket.cat', username: 'rocket.cat', }, + u: { + _id: '3Wz2wANqwrd7Hu5Fo', + username: 'dgubert', + name: 'Douglas Gubert', + }, }, }; diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/Users.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/Users.mock.ts similarity index 72% rename from apps/meteor/tests/unit/app/apps/server/mocks/models/Users.mock.js rename to apps/meteor/tests/unit/app/apps/server/mocks/models/Users.mock.ts index 8454d16fd01e5..f0f9dcf46f7f6 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/Users.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/Users.mock.ts @@ -1,15 +1,17 @@ +import { UserStatus, type IUser } from '@rocket.chat/core-typings'; + import { BaseModelMock } from './BaseModel.mock'; -export class UsersMock extends BaseModelMock { - data = { +export class UsersMock extends BaseModelMock { + override data = { 'rocket.cat': { _id: 'rocket.cat', createdAt: new Date('2019-03-27T20:51:36.821Z'), avatarOrigin: 'local', name: 'Rocket.Cat', username: 'rocket.cat', - status: 'online', - statusDefault: 'online', + status: UserStatus.ONLINE, + statusDefault: UserStatus.ONLINE, utcOffset: 0, active: true, type: 'bot', @@ -32,8 +34,8 @@ export class UsersMock extends BaseModelMock { isEnabled: true, name: 'Rocket.Cat', roles: ['bot'], - status: 'online', - statusConnection: 'online', + status: UserStatus.ONLINE, + statusConnection: UserStatus.ONLINE, utcOffset: 0, createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/index.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/index.ts similarity index 100% rename from apps/meteor/tests/unit/app/apps/server/mocks/models/index.js rename to apps/meteor/tests/unit/app/apps/server/mocks/models/index.ts diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.ts similarity index 56% rename from apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js rename to apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.ts index 79e8996598bc5..5f89cb33a8fcf 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/orchestrator.mock.ts @@ -1,11 +1,28 @@ /* istanbul ignore file */ export class AppServerOrchestratorMock { + private _marketplaceClient: Record; + + private _model: Record; + + private _persistModel: Record; + + private _storage: Record; + + private _logStorage: Record; + + private _converters: Map>; + + private _bridges: Record; + + private _manager: { areAppsLoaded?: () => boolean; load?: () => Promise; unload?: () => Promise }; + + private _communicators: Map>; + constructor() { this._marketplaceClient = {}; this._model = {}; - this._logModel = {}; this._persistModel = {}; this._storage = {}; this._logStorage = {}; @@ -26,61 +43,61 @@ export class AppServerOrchestratorMock { this._communicators.set('restapi', {}); } - getMarketplaceClient() { + getMarketplaceClient(): Record { return this._marketplaceClient; } - getModel() { + getModel(): Record { return this._model; } - getPersistenceModel() { + getPersistenceModel(): Record { return this._persistModel; } - getStorage() { + getStorage(): Record { return this._storage; } - getLogStorage() { + getLogStorage(): Record { return this._logStorage; } - getConverters() { + getConverters(): Map> { return this._converters; } - getBridges() { + getBridges(): Record { return this._bridges; } - getNotifier() { + getNotifier(): Record | undefined { return this._communicators.get('notifier'); } - getManager() { + getManager(): { areAppsLoaded?: () => boolean; load?: () => Promise; unload?: () => Promise } { return this._manager; } - isEnabled() { + isEnabled(): boolean { return true; } - isLoaded() { - return this.getManager().areAppsLoaded(); + isLoaded(): boolean { + return this.getManager().areAppsLoaded?.() ?? false; } - isDebugging() { + isDebugging(): boolean { return true; } - debugLog(...args) { + debugLog(...args: unknown[]): void { if (this.isDebugging()) { console.log(...args); } } - load() { + load(): void { // Don't try to load it again if it has // already been loaded if (this.isLoaded()) { @@ -88,12 +105,12 @@ export class AppServerOrchestratorMock { } this._manager - .load() + .load?.() .then((affs) => console.log(`Loaded the Apps Framework and loaded a total of ${affs.length} Apps!`)) .catch((err) => console.warn('Failed to load the Apps Framework and Apps!', err)); } - unload() { + unload(): void { // Don't try to unload it if it's already been // unlaoded or wasn't unloaded to start with if (!this.isLoaded()) { @@ -101,7 +118,7 @@ export class AppServerOrchestratorMock { } this._manager - .unload() + .unload?.() .then(() => console.log('Unloaded the Apps Framework.')) .catch((err) => console.warn('Failed to unload the Apps Framework!', err)); } diff --git a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts index fa2dabcb20313..62033eb7a4a7b 100644 --- a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts +++ b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts @@ -31,21 +31,26 @@ describe('The AppMessagesConverter instance', () => { const usersConverter = orchestrator.getConverters().get('users'); - usersConverter.convertById = function convertUserByIdStub(id: string) { - return UsersMock.convertedData[id as 'rocket.cat'] || undefined; - }; - - usersConverter.convertToApp = function convertUserToAppStub(user: UsersMock['data']['rocket.cat']) { - return { - id: user._id, - username: user.username, - name: user.name, + if (usersConverter) { + usersConverter.convertById = function convertUserByIdStub(id: string) { + return UsersMock.convertedData[id as 'rocket.cat'] || undefined; }; - }; - orchestrator.getConverters().get('messages').convertById = async function convertRoomByIdStub(_id: string) { - return {}; - }; + usersConverter.convertToApp = function convertUserToAppStub(user: UsersMock['data']['rocket.cat']) { + return { + id: user._id, + username: user.username, + name: user.name, + }; + }; + } + + const messagesConverter = orchestrator.getConverters().get('messages'); + if (messagesConverter) { + messagesConverter.convertById = async function convertRoomByIdStub(_id: string) { + return {}; + }; + } roomConverter = new AppRoomsConverter(orchestrator); roomsMock = new RoomsMock(); From b60ab8fbf7a416b5386dab90ca5284a4d2da4303 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sun, 1 Feb 2026 00:13:23 -0300 Subject: [PATCH 68/80] refactor: migrate unit test `apps/meteor/tests/unit/app/apps/server/messages.tests.js` --- apps/meteor/.mocharc.js | 3 - .../apps/server/converters/messages.spec.ts | 185 ++++++++++++++++++ apps/meteor/jest.config.ts | 1 + .../unit/app/apps/server/messages.tests.js | 179 ----------------- 4 files changed, 186 insertions(+), 182 deletions(-) create mode 100644 apps/meteor/app/apps/server/converters/messages.spec.ts delete mode 100644 apps/meteor/tests/unit/app/apps/server/messages.tests.js diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index ab26d7c50e9d2..f23763955406f 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -29,8 +29,5 @@ module.exports = { 'app/api/server/lib/**/*.spec.ts', 'app/file-upload/server/**/*.spec.ts', 'app/livechat/server/lib/**/*.spec.ts', - '!app/mentions/server/**/*.spec.ts', - '!app/mentions/lib/**/*.spec.ts', - '!app/utils/lib/**/*.spec.ts', ], }; diff --git a/apps/meteor/app/apps/server/converters/messages.spec.ts b/apps/meteor/app/apps/server/converters/messages.spec.ts new file mode 100644 index 0000000000000..80d9bfc356ec4 --- /dev/null +++ b/apps/meteor/app/apps/server/converters/messages.spec.ts @@ -0,0 +1,185 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +import { + appMessageMock, + appMessageInvalidRoomMock, + appPartialMessageMock, +} from '../../../../tests/unit/app/apps/server/mocks/data/messages.data'; +import { MessagesMock } from '../../../../tests/unit/app/apps/server/mocks/models/Messages.mock'; +import { RoomsMock } from '../../../../tests/unit/app/apps/server/mocks/models/Rooms.mock'; +import { UsersMock } from '../../../../tests/unit/app/apps/server/mocks/models/Users.mock'; +import { AppServerOrchestratorMock } from '../../../../tests/unit/app/apps/server/mocks/orchestrator.mock'; + +jest.mock('@rocket.chat/models', () => ({ + Rooms: new RoomsMock(), + Messages: new MessagesMock(), + Users: new UsersMock(), +})); + +jest.mock('@rocket.chat/core-typings', () => ({ + ...jest.requireActual('@rocket.chat/core-typings'), + isMessageFromVisitor: (message: unknown) => message && typeof message === 'object' && 'token' in message, +})); + +describe('The AppMessagesConverter instance', () => { + let AppMessagesConverter: any; + let messagesConverter: any; + let messagesMock: MessagesMock; + + beforeAll(async () => { + const module = await import('./messages'); + AppMessagesConverter = module.AppMessagesConverter; + }); + + beforeEach(() => { + const orchestrator = new AppServerOrchestratorMock(); + + const usersConverter = orchestrator.getConverters().get('users'); + + if (usersConverter) { + usersConverter.convertById = function convertUserByIdStub(id: string) { + return UsersMock.convertedData[id as 'rocket.cat']; + }; + + usersConverter.convertToApp = function convertUserToAppStub(user: UsersMock['data']['rocket.cat']) { + return { + id: user._id, + username: user.username, + name: user.name, + }; + }; + } + + const roomsConverter = orchestrator.getConverters().get('rooms'); + if (roomsConverter) { + roomsConverter.convertById = async function convertRoomByIdStub(id: string) { + return RoomsMock.convertedData[id as 'GENERAL']; + }; + } + + messagesConverter = new AppMessagesConverter(orchestrator); + messagesMock = new MessagesMock(); + }); + + const createdAt = new Date('2019-03-30T01:22:08.389Z'); + const updatedAt = new Date('2019-03-30T01:22:08.412Z'); + + describe('when converting a message from Rocket.Chat to the Engine schema', () => { + it('should return `undefined` when `msgObj` is falsy', async () => { + const appMessage = await messagesConverter.convertMessage(undefined); + + expect(appMessage).toBeUndefined(); + }); + + it('should return a proper schema', async () => { + const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('SimpleMessageMock')); + + expect(appMessage).toHaveProperty('id', 'SimpleMessageMock'); + expect(appMessage.createdAt).toEqual(createdAt); + expect(appMessage.updatedAt).toEqual(updatedAt); + expect(appMessage).toHaveProperty('groupable', false); + expect(appMessage.sender).toMatchObject({ id: 'rocket.cat' }); + expect(appMessage.room).toMatchObject({ id: 'GENERAL' }); + + expect(appMessage).not.toHaveProperty('editor'); + expect(appMessage).not.toHaveProperty('attachments'); + expect(appMessage).not.toHaveProperty('reactions'); + expect(appMessage).not.toHaveProperty('avatarUrl'); + expect(appMessage).not.toHaveProperty('alias'); + expect(appMessage).not.toHaveProperty('customFields'); + expect(appMessage).not.toHaveProperty('emoji'); + }); + + it('should not mutate the original message object', async () => { + const rocketchatMessageMock = messagesMock.findOneById('SimpleMessageMock') as IMessage; + + await messagesConverter.convertMessage(rocketchatMessageMock); + + expect(rocketchatMessageMock).toEqual({ + _id: 'SimpleMessageMock', + t: 'uj', + rid: 'GENERAL', + ts: new Date('2019-03-30T01:22:08.389Z'), + msg: 'rocket.cat', + u: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, + groupable: false, + _updatedAt: new Date('2019-03-30T01:22:08.412Z'), + }); + }); + + it("should return basic sender info when it's not a Rocket.Chat user (e.g. Livechat Guest)", async () => { + const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('LivechatGuestMessageMock')); + + expect(appMessage.sender).toMatchObject({ + id: 'guest1234', + username: 'guest1234', + name: 'Livechat Guest', + }); + }); + }); + + describe('when converting a message from the Engine schema back to Rocket.Chat', () => { + it('should return `undefined` when `message` is falsy', async () => { + const rocketchatMessage = await messagesConverter.convertAppMessage(undefined); + + expect(rocketchatMessage).toBeUndefined(); + }); + + it('should return a proper schema', async () => { + const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock); + + expect(rocketchatMessage).toHaveProperty('_id', 'appMessageMock'); + expect(rocketchatMessage).toHaveProperty('rid', 'GENERAL'); + expect(rocketchatMessage).toHaveProperty('groupable', false); + expect(rocketchatMessage.ts).toEqual(createdAt); + expect(rocketchatMessage._updatedAt).toEqual(updatedAt); + expect(rocketchatMessage.u).toMatchObject({ + _id: 'rocket.cat', + username: 'rocket.cat', + name: 'Rocket.Cat', + }); + }); + + it('should return a proper schema when receiving a partial object', async () => { + const rocketchatMessage = await messagesConverter.convertAppMessage(appPartialMessageMock, true); + + expect(rocketchatMessage).toHaveProperty('_id', 'appPartialMessageMock'); + expect(rocketchatMessage).toHaveProperty('groupable', false); + expect(rocketchatMessage).toHaveProperty('emoji', ':smirk:'); + expect(rocketchatMessage).toHaveProperty('alias', 'rocket.feline'); + + expect(rocketchatMessage).not.toHaveProperty('ts'); + expect(rocketchatMessage).not.toHaveProperty('u'); + expect(rocketchatMessage).not.toHaveProperty('rid'); + expect(rocketchatMessage).not.toHaveProperty('_updatedAt'); + }); + + it('should merge `_unmappedProperties_` into the returned message', async () => { + const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock); + + expect(rocketchatMessage).not.toHaveProperty('_unmappedProperties_'); + expect(rocketchatMessage).toHaveProperty('t', 'uj'); + }); + + it('should not merge `_unmappedProperties_` into the returned message when receiving a partial object', async () => { + const invalidPartialMessage = structuredClone(appPartialMessageMock) as typeof appPartialMessageMock & { + _unmappedProperties_?: Record; + }; + invalidPartialMessage._unmappedProperties_ = { + t: 'uj', + }; + + const rocketchatMessage = await messagesConverter.convertAppMessage(invalidPartialMessage, true); + + expect(rocketchatMessage).not.toHaveProperty('_unmappedProperties_'); + expect(rocketchatMessage).not.toHaveProperty('t'); + }); + + it('should throw if message has an invalid room', async () => { + await expect(messagesConverter.convertAppMessage(appMessageInvalidRoomMock)).rejects.toThrow('Invalid room provided on the message.'); + }); + }); +}); diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index d0f25f8c1ea94..151ad84aeda7a 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -44,6 +44,7 @@ export default { '/app/mentions/server/*.spec.ts', '/app/statistics/**/*.spec.ts', '/app/*/lib/**/*.spec.ts', + '/app/apps/server/converters/*.spec.ts', '/server/lib/auditServerEvents/**.spec.ts', '/server/services/import/**/*.spec.ts', '/server/settings/lib/**.spec.ts', diff --git a/apps/meteor/tests/unit/app/apps/server/messages.tests.js b/apps/meteor/tests/unit/app/apps/server/messages.tests.js deleted file mode 100644 index 29921ed84a790..0000000000000 --- a/apps/meteor/tests/unit/app/apps/server/messages.tests.js +++ /dev/null @@ -1,179 +0,0 @@ -import { expect } from 'chai'; -import proxyquire from 'proxyquire'; - -import { appMessageMock, appMessageInvalidRoomMock, appPartialMessageMock } from './mocks/data/messages.data'; -import { MessagesMock } from './mocks/models/Messages.mock'; -import { RoomsMock } from './mocks/models/Rooms.mock'; -import { UsersMock } from './mocks/models/Users.mock'; -import { AppServerOrchestratorMock } from './mocks/orchestrator.mock'; - -const { AppMessagesConverter } = proxyquire.noCallThru().load('../../../../../app/apps/server/converters/messages', { - '@rocket.chat/random': { - Random: { - id: () => 1, - }, - }, - '@rocket.chat/models': { - Rooms: new RoomsMock(), - Messages: new MessagesMock(), - Users: new UsersMock(), - }, - '@rocket.chat/core-typings': { - isMessageFromVisitor: (message) => 'token' in message, - }, -}); - -describe('The AppMessagesConverter instance', () => { - let messagesConverter; - let messagesMock; - - before(() => { - const orchestrator = new AppServerOrchestratorMock(); - - const usersConverter = orchestrator.getConverters().get('users'); - - usersConverter.convertById = function convertUserByIdStub(id) { - return UsersMock.convertedData[id]; - }; - - usersConverter.convertToApp = function convertUserToAppStub(user) { - return { - id: user._id, - username: user.username, - name: user.name, - }; - }; - - orchestrator.getConverters().get('rooms').convertById = async function convertRoomByIdStub(id) { - return RoomsMock.convertedData[id]; - }; - - messagesConverter = new AppMessagesConverter(orchestrator); - messagesMock = new MessagesMock(); - }); - - const createdAt = new Date('2019-03-30T01:22:08.389Z'); - const updatedAt = new Date('2019-03-30T01:22:08.412Z'); - - describe('when converting a message from Rocket.Chat to the Engine schema', () => { - it('should return `undefined` when `msgObj` is falsy', async () => { - const appMessage = await messagesConverter.convertMessage(undefined); - - expect(appMessage).to.be.undefined; - }); - - it('should return a proper schema', async () => { - const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('SimpleMessageMock')); - - expect(appMessage).to.have.property('id', 'SimpleMessageMock'); - expect(appMessage).to.have.property('createdAt').which.equalTime(createdAt); - expect(appMessage).to.have.property('updatedAt').which.equalTime(updatedAt); - expect(appMessage).to.have.property('groupable', false); - expect(appMessage).to.have.property('sender').which.includes({ id: 'rocket.cat' }); - expect(appMessage).to.have.property('room').which.includes({ id: 'GENERAL' }); - - expect(appMessage).not.to.have.property('editor'); - expect(appMessage).not.to.have.property('attachments'); - expect(appMessage).not.to.have.property('reactions'); - expect(appMessage).not.to.have.property('avatarUrl'); - expect(appMessage).not.to.have.property('alias'); - expect(appMessage).not.to.have.property('customFields'); - expect(appMessage).not.to.have.property('emoji'); - }); - - it('should not mutate the original message object', () => { - const rocketchatMessageMock = messagesMock.findOneById('SimpleMessageMock'); - - messagesConverter.convertMessage(rocketchatMessageMock); - - expect(rocketchatMessageMock).to.deep.equal({ - _id: 'SimpleMessageMock', - t: 'uj', - rid: 'GENERAL', - ts: new Date('2019-03-30T01:22:08.389Z'), - msg: 'rocket.cat', - u: { - _id: 'rocket.cat', - username: 'rocket.cat', - }, - groupable: false, - _updatedAt: new Date('2019-03-30T01:22:08.412Z'), - }); - }); - - it("should return basic sender info when it's not a Rocket.Chat user (e.g. Livechat Guest)", async () => { - const appMessage = await messagesConverter.convertMessage(messagesMock.findOneById('LivechatGuestMessageMock')); - - expect(appMessage).to.have.property('sender').which.includes({ - id: 'guest1234', - username: 'guest1234', - name: 'Livechat Guest', - }); - }); - }); - - describe('when converting a message from the Engine schema back to Rocket.Chat', () => { - it('should return `undefined` when `message` is falsy', async () => { - const rocketchatMessage = await messagesConverter.convertAppMessage(undefined); - - expect(rocketchatMessage).to.be.undefined; - }); - - it('should return a proper schema', async () => { - const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock); - - expect(rocketchatMessage).to.have.property('_id', 'appMessageMock'); - expect(rocketchatMessage).to.have.property('rid', 'GENERAL'); - expect(rocketchatMessage).to.have.property('groupable', false); - expect(rocketchatMessage).to.have.property('ts').which.equalTime(createdAt); - expect(rocketchatMessage).to.have.property('_updatedAt').which.equalTime(updatedAt); - expect(rocketchatMessage).to.have.property('u').which.includes({ - _id: 'rocket.cat', - username: 'rocket.cat', - name: 'Rocket.Cat', - }); - }); - - it('should return a proper schema when receiving a partial object', async () => { - const rocketchatMessage = await messagesConverter.convertAppMessage(appPartialMessageMock, true); - - expect(rocketchatMessage).to.have.property('_id', 'appPartialMessageMock'); - expect(rocketchatMessage).to.have.property('groupable', false); - expect(rocketchatMessage).to.have.property('emoji', ':smirk:'); - expect(rocketchatMessage).to.have.property('alias', 'rocket.feline'); - - expect(rocketchatMessage).to.not.have.property('ts'); - expect(rocketchatMessage).to.not.have.property('u'); - expect(rocketchatMessage).to.not.have.property('rid'); - expect(rocketchatMessage).to.not.have.property('_updatedAt'); - }); - - it('should merge `_unmappedProperties_` into the returned message', async () => { - const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock); - - expect(rocketchatMessage).not.to.have.property('_unmappedProperties_'); - expect(rocketchatMessage).to.have.property('t', 'uj'); - }); - - it('should not merge `_unmappedProperties_` into the returned message when receiving a partial object', async () => { - const invalidPartialMessage = structuredClone(appPartialMessageMock); - invalidPartialMessage._unmappedProperties_ = { - t: 'uj', - }; - - const rocketchatMessage = await messagesConverter.convertAppMessage(invalidPartialMessage, true); - - expect(rocketchatMessage).to.not.have.property('_unmappedProperties_'); - expect(rocketchatMessage).to.not.have.property('t'); - }); - - it('should throw if message has an invalid room', async () => { - try { - await messagesConverter.convertAppMessage(appMessageInvalidRoomMock); - } catch (e) { - expect(e).to.be.an.instanceOf(Error); - expect(e.message).to.equal('Invalid room provided on the message.'); - } - }); - }); -}); From 8e702c19f6d528a73f52065dfc426cdb975e3f50 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sun, 1 Feb 2026 00:32:55 -0300 Subject: [PATCH 69/80] refactor: migrate unit test `apps/meteor/tests/unit/app/apps/server/rooms.tests.ts` --- .../apps/server/converters/rooms.spec.ts} | 124 +++++++++--------- 1 file changed, 64 insertions(+), 60 deletions(-) rename apps/meteor/{tests/unit/app/apps/server/rooms.tests.ts => app/apps/server/converters/rooms.spec.ts} (57%) diff --git a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts b/apps/meteor/app/apps/server/converters/rooms.spec.ts similarity index 57% rename from apps/meteor/tests/unit/app/apps/server/rooms.tests.ts rename to apps/meteor/app/apps/server/converters/rooms.spec.ts index 62033eb7a4a7b..1c87957e17d16 100644 --- a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts +++ b/apps/meteor/app/apps/server/converters/rooms.spec.ts @@ -1,32 +1,34 @@ import type { IAppRoomsConverter, IAppsRoom } from '@rocket.chat/apps'; import type { IRoom } from '@rocket.chat/core-typings'; -import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; -import proxyquire from 'proxyquire'; - -import { MessagesMock } from './mocks/models/Messages.mock'; -import { RoomsMock } from './mocks/models/Rooms.mock'; -import { UsersMock } from './mocks/models/Users.mock'; -import { AppServerOrchestratorMock } from './mocks/orchestrator.mock'; - -const { AppRoomsConverter } = proxyquire.noCallThru().load('../../../../../app/apps/server/converters/rooms', { - '@rocket.chat/random': { - Random: { - id: () => 1, - }, - }, - '@rocket.chat/models': { - Rooms: new RoomsMock(), - Messages: new MessagesMock(), - Users: new UsersMock(), + +import { MessagesMock } from '../../../../tests/unit/app/apps/server/mocks/models/Messages.mock'; +import { RoomsMock } from '../../../../tests/unit/app/apps/server/mocks/models/Rooms.mock'; +import { UsersMock } from '../../../../tests/unit/app/apps/server/mocks/models/Users.mock'; +import { AppServerOrchestratorMock } from '../../../../tests/unit/app/apps/server/mocks/orchestrator.mock'; + +jest.mock('@rocket.chat/random', () => ({ + Random: { + id: () => 1, }, -}); +})); + +jest.mock('@rocket.chat/models', () => ({ + Rooms: new RoomsMock(), + Messages: new MessagesMock(), + Users: new UsersMock(), +})); describe('The AppMessagesConverter instance', () => { + let AppRoomsConverter: any; let roomConverter: IAppRoomsConverter; let roomsMock: RoomsMock; - before(() => { + beforeAll(async () => { + const module = await import('./rooms'); + AppRoomsConverter = module.AppRoomsConverter; + }); + + beforeEach(() => { const orchestrator = new AppServerOrchestratorMock(); const usersConverter = orchestrator.getConverters().get('users'); @@ -60,19 +62,19 @@ describe('The AppMessagesConverter instance', () => { it('should return `undefined` when `originalRoom` is falsy', async () => { const appRoom = await roomConverter.convertRoom(undefined); - expect(appRoom).to.be.undefined; + expect(appRoom).toBeUndefined(); }); it('should return a proper schema', async () => { const mockedRoom = roomsMock.findOneById('GENERAL') as RoomsMock['data']['GENERAL']; const appRoom = await roomConverter.convertRoom(mockedRoom as unknown as IRoom); - expect(appRoom).to.have.property('id', mockedRoom._id); - expect(appRoom).to.have.property('type', mockedRoom.t); - expect(appRoom).to.have.property('slugifiedName', mockedRoom.name); - expect(appRoom).to.have.property('createdAt').which.equalTime(mockedRoom.ts); - expect(appRoom).to.have.property('updatedAt').which.equalTime(mockedRoom._updatedAt); - expect(appRoom).to.have.property('messageCount', mockedRoom.msgs); + expect(appRoom).toHaveProperty('id', mockedRoom._id); + expect(appRoom).toHaveProperty('type', mockedRoom.t); + expect(appRoom).toHaveProperty('slugifiedName', mockedRoom.name); + expect(appRoom?.createdAt).toEqual(mockedRoom.ts); + expect(appRoom?.updatedAt).toEqual(mockedRoom._updatedAt); + expect(appRoom).toHaveProperty('messageCount', mockedRoom.msgs); }); it('should not mutate the original room object', async () => { @@ -80,14 +82,16 @@ describe('The AppMessagesConverter instance', () => { await roomConverter.convertRoom(rocketchatRoomMock); - expect(rocketchatRoomMock).to.deep.equal(roomsMock.findOneById('GENERAL')); + expect(rocketchatRoomMock).toEqual(roomsMock.findOneById('GENERAL')); }); it('should add an `_unmappedProperties_` field to the converted room which contains the `lastMessage` property of the room', async () => { const mockedRoom = roomsMock.findOneById('GENERAL') as RoomsMock['data']['GENERAL']; const appMessage = await roomConverter.convertRoom(mockedRoom as unknown as IRoom); - expect(appMessage).to.have.property('_unmappedProperties_').which.has.property('lastMessage').to.deep.equal(mockedRoom.lastMessage); + expect(appMessage).toHaveProperty('_unmappedProperties_'); + expect((appMessage as any)._unmappedProperties_).toHaveProperty('lastMessage'); + expect((appMessage as any)._unmappedProperties_.lastMessage).toEqual(mockedRoom.lastMessage); }); }); @@ -95,72 +99,72 @@ describe('The AppMessagesConverter instance', () => { it('should return `undefined` when `room` is falsy', async () => { const rocketchatMessage = await roomConverter.convertAppRoom(undefined); - expect(rocketchatMessage).to.be.undefined; + expect(rocketchatMessage).toBeUndefined(); }); it('should return a proper schema', async () => { const appRoom = RoomsMock.convertedData.GENERAL as unknown as IAppsRoom; const rocketchatRoom = await roomConverter.convertAppRoom(appRoom); - expect(rocketchatRoom).to.have.property('_id', appRoom.id); - expect(rocketchatRoom).to.have.property('ts', appRoom.createdAt); - expect(rocketchatRoom).to.have.property('lm', appRoom.lastModifiedAt); - expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); - expect(rocketchatRoom).to.have.property('t', appRoom.type); - expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); + expect(rocketchatRoom).toHaveProperty('_id', appRoom.id); + expect(rocketchatRoom).toHaveProperty('ts', appRoom.createdAt); + expect(rocketchatRoom).toHaveProperty('lm', appRoom.lastModifiedAt); + expect(rocketchatRoom).toHaveProperty('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).toHaveProperty('t', appRoom.type); + expect(rocketchatRoom).toHaveProperty('name', appRoom.slugifiedName); }); it('should return a proper schema when receiving a partial object', async () => { const appRoom = RoomsMock.convertedData.GENERALPartial as unknown as IAppsRoom; const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); - expect(rocketchatRoom).to.have.property('_id', appRoom.id); - expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); - expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); - expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).toHaveProperty('_id', appRoom.id); + expect(rocketchatRoom).toHaveProperty('name', appRoom.slugifiedName); + expect(rocketchatRoom).toHaveProperty('sysMes', appRoom.displaySystemMessages); + expect(rocketchatRoom).toHaveProperty('_updatedAt', appRoom.updatedAt); - expect(rocketchatRoom).to.not.have.property('msgs'); - expect(rocketchatRoom).to.not.have.property('ro'); - expect(rocketchatRoom).to.not.have.property('default'); - expect(rocketchatRoom).to.not.have.property('t'); + expect(rocketchatRoom).not.toHaveProperty('msgs'); + expect(rocketchatRoom).not.toHaveProperty('ro'); + expect(rocketchatRoom).not.toHaveProperty('default'); + expect(rocketchatRoom).not.toHaveProperty('t'); }); it('should return a proper schema when receiving a partial object', async () => { const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); - expect(rocketchatRoom).to.have.property('_id', appRoom.id); - expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); - expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); - expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); - expect(rocketchatRoom).to.have.property('msgs', appRoom.messageCount); - expect(rocketchatRoom).to.have.property('t', 'c'); + expect(rocketchatRoom).toHaveProperty('_id', appRoom.id); + expect(rocketchatRoom).toHaveProperty('name', appRoom.slugifiedName); + expect(rocketchatRoom).toHaveProperty('sysMes', appRoom.displaySystemMessages); + expect(rocketchatRoom).toHaveProperty('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).toHaveProperty('msgs', appRoom.messageCount); + expect(rocketchatRoom).toHaveProperty('t', 'c'); - expect(rocketchatRoom).to.not.have.property('ro'); - expect(rocketchatRoom).to.not.have.property('default'); + expect(rocketchatRoom).not.toHaveProperty('ro'); + expect(rocketchatRoom).not.toHaveProperty('default'); }); it('should not include properties that are not present in the app room', async () => { const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom; const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); - expect(rocketchatRoom).to.have.property('customFields'); - expect(rocketchatRoom).to.not.have.property('_id'); - expect(rocketchatRoom).to.not.have.property('t'); + expect(rocketchatRoom).toHaveProperty('customFields'); + expect(rocketchatRoom).not.toHaveProperty('_id'); + expect(rocketchatRoom).not.toHaveProperty('t'); }); it('should not include name as undefined if the room doesnt have a name property', async () => { const appRoom = RoomsMock.convertedData.UpdatedRoom as unknown as IAppsRoom; const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); - expect(rocketchatRoom.name).to.be.undefined; + expect(rocketchatRoom?.name).toBeUndefined(); }); it('should include a name if the source room has slugifiedName property', async () => { const appRoom = RoomsMock.convertedData.GENERALPartialWithOptionalProps as unknown as IAppsRoom; const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); - expect(rocketchatRoom.name).to.equal(appRoom.slugifiedName); + expect(rocketchatRoom?.name).toBe(appRoom.slugifiedName); }); it('should not use _unmappedProperties when the room is a partial object', async () => { @@ -168,7 +172,7 @@ describe('The AppMessagesConverter instance', () => { // @ts-expect-error - _unmappedProperties const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, true); - expect(rocketchatRoom).to.not.have.property('unmapped'); + expect(rocketchatRoom).not.toHaveProperty('unmapped'); }); it('should use _unmappedProperties when the room is a partial object', async () => { @@ -176,7 +180,7 @@ describe('The AppMessagesConverter instance', () => { // @ts-expect-error - _unmappedProperties const rocketchatRoom = await roomConverter.convertAppRoom({ ...appRoom, _unmappedProperties_: { unmapped: 'property' } }, false); - expect(rocketchatRoom).to.have.property('unmapped', 'property'); + expect(rocketchatRoom).toHaveProperty('unmapped', 'property'); }); }); }); From 2132361d973fd3c49cdf0d90d150fcfa7fc29373 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sun, 1 Feb 2026 00:44:57 -0300 Subject: [PATCH 70/80] refactor: delete useless unit test --- apps/meteor/.mocharc.js | 1 - .../tests/unit/app/ui-utils/server.tests.js | 18 ------------------ 2 files changed, 19 deletions(-) delete mode 100644 apps/meteor/tests/unit/app/ui-utils/server.tests.js diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index f23763955406f..402af406ba7f6 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -19,7 +19,6 @@ module.exports = { 'ee/tests/**/*.tests.ts', 'ee/tests/**/*.spec.ts', 'tests/unit/app/**/*.spec.ts', - 'tests/unit/app/**/*.tests.js', 'tests/unit/app/**/*.tests.ts', 'tests/unit/lib/**/*.tests.ts', 'server/routes/avatar/**/*.spec.ts', diff --git a/apps/meteor/tests/unit/app/ui-utils/server.tests.js b/apps/meteor/tests/unit/app/ui-utils/server.tests.js deleted file mode 100644 index e259816ac2509..0000000000000 --- a/apps/meteor/tests/unit/app/ui-utils/server.tests.js +++ /dev/null @@ -1,18 +0,0 @@ -import { expect } from 'chai'; - -const messages = { - 'Sample Message': 14, - 'Sample 1 ⛳': 10, - 'Sample 2 ❤': 10, - 'Sample 3 ⛳❤⛳❤': 13, -}; - -describe('Message Properties', () => { - describe('Check Message Length', () => { - Object.keys(messages).forEach((objectKey) => { - it('should treat emojis as single characters', () => { - expect(objectKey.length).to.be.equal(messages[objectKey]); - }); - }); - }); -}); From 4541f5e49bb2293549f68aa6971e947a3d40cd93 Mon Sep 17 00:00:00 2001 From: Tasso Date: Sun, 1 Feb 2026 00:52:04 -0300 Subject: [PATCH 71/80] refactor: migrate `apps/meteor/.storybook/mocks/meteor.js` to TypeScript --- apps/meteor/.storybook/main.ts | 4 +- apps/meteor/.storybook/mocks/meteor.js | 95 -------------------------- apps/meteor/.storybook/mocks/meteor.ts | 95 ++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 97 deletions(-) delete mode 100644 apps/meteor/.storybook/mocks/meteor.js create mode 100644 apps/meteor/.storybook/mocks/meteor.ts diff --git a/apps/meteor/.storybook/main.ts b/apps/meteor/.storybook/main.ts index 1cceaff3ff602..9c0d6fd665cf3 100644 --- a/apps/meteor/.storybook/main.ts +++ b/apps/meteor/.storybook/main.ts @@ -4,7 +4,7 @@ import type { StorybookConfig } from '@storybook/react-webpack5'; import webpack from 'webpack'; export default { - stories: ['../client/**/*.stories.{js,tsx}', '../app/**/*.stories.{js,tsx}', '../ee/app/**/*.stories.{js,tsx}'], + stories: ['../client/**/*.stories.{js,tsx}'], addons: [ getAbsolutePath('@storybook/addon-essentials'), @@ -44,7 +44,7 @@ export default { }); config.plugins?.push( - new webpack.NormalModuleReplacementPlugin(/^meteor/, require.resolve('./mocks/meteor.js')), + new webpack.NormalModuleReplacementPlugin(/^meteor/, require.resolve('./mocks/meteor.ts')), new webpack.NormalModuleReplacementPlugin(/(app)\/*.*\/(server)\/*/, require.resolve('./mocks/empty.ts')), ); diff --git a/apps/meteor/.storybook/mocks/meteor.js b/apps/meteor/.storybook/mocks/meteor.js deleted file mode 100644 index 12c90ae1501bf..0000000000000 --- a/apps/meteor/.storybook/mocks/meteor.js +++ /dev/null @@ -1,95 +0,0 @@ -export const Meteor = { - Device: { - isDesktop: () => false, - }, - isClient: true, - isServer: false, - _localStorage: window.localStorage, - absoluteUrl: Object.assign(() => {}, { - defaultOptions: {}, - }), - userId: () => {}, - Streamer: () => ({ - on: () => {}, - removeListener: () => {}, - }), - StreamerCentral: { - on: () => {}, - removeListener: () => {}, - }, - startup: () => {}, - methods: () => {}, - call: () => {}, - connection: { - _stream: { - on: () => {}, - }, - }, - users: {}, -}; - -export const Tracker = { - autorun: () => ({ - stop: () => {}, - }), - nonreactive: (fn) => fn(), - Dependency: () => {}, -}; - -export const Accounts = { - onLogin: () => {}, - onLogout: () => {}, -}; - -export const Mongo = { - Collection: () => ({ - find: () => ({ - observe: () => {}, - fetch: () => [], - }), - }), -}; - -export const ReactiveVar = (val) => { - let currentVal = val; - return { - get: () => currentVal, - set: (val) => { - currentVal = val; - }, - }; -}; - -export const ReactiveDict = () => ({ - get: () => {}, - set: () => {}, - all: () => {}, -}); - -export const Template = Object.assign( - () => ({ - onCreated: () => {}, - onRendered: () => {}, - onDestroyed: () => {}, - helpers: () => {}, - events: () => {}, - }), - { - registerHelper: () => {}, - __checkName: () => {}, - }, -); - -export const check = () => {}; - -export const FlowRouter = { - route: () => {}, - group: () => ({ - route: () => {}, - }), -}; - -export const Session = { - get: () => {}, - set: () => {}, -}; diff --git a/apps/meteor/.storybook/mocks/meteor.ts b/apps/meteor/.storybook/mocks/meteor.ts new file mode 100644 index 0000000000000..4e39fe69ded33 --- /dev/null +++ b/apps/meteor/.storybook/mocks/meteor.ts @@ -0,0 +1,95 @@ +export const Meteor = { + Device: { + isDesktop: () => false, + }, + isClient: true, + isServer: false, + _localStorage: window.localStorage, + absoluteUrl: Object.assign(() => undefined, { + defaultOptions: {}, + }), + userId: () => undefined, + Streamer: () => ({ + on: () => undefined, + removeListener: () => undefined, + }), + StreamerCentral: { + on: () => undefined, + removeListener: () => undefined, + }, + startup: () => undefined, + methods: () => undefined, + call: () => undefined, + connection: { + _stream: { + on: () => undefined, + }, + }, + users: {}, +}; + +export const Tracker = { + autorun: () => ({ + stop: () => undefined, + }), + nonreactive: (fn: () => void) => fn(), + Dependency: () => undefined, +}; + +export const Accounts = { + onLogin: () => undefined, + onLogout: () => undefined, +}; + +export const Mongo = { + Collection: () => ({ + find: () => ({ + observe: () => undefined, + fetch: () => [], + }), + }), +}; + +export const ReactiveVar = (val: T) => { + let currentVal = val; + return { + get: () => currentVal, + set: (val: T) => { + currentVal = val; + }, + }; +}; + +export const ReactiveDict = () => ({ + get: () => undefined, + set: () => undefined, + all: () => undefined, +}); + +export const Template = Object.assign( + () => ({ + onCreated: () => undefined, + onRendered: () => undefined, + onDestroyed: () => undefined, + helpers: () => undefined, + events: () => undefined, + }), + { + registerHelper: () => undefined, + __checkName: () => undefined, + }, +); + +export const check = () => undefined; + +export const FlowRouter = { + route: () => undefined, + group: () => ({ + route: () => undefined, + }), +}; + +export const Session = { + get: () => undefined, + set: () => undefined, +}; From f31c64866e99ed648344222d2dc7dadb7b6745b2 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:04:08 -0300 Subject: [PATCH 72/80] refactor: review over `apps/meteor/app/apps/server/converters/visitors.ts` --- apps/meteor/app/apps/server/converters/visitors.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/apps/server/converters/visitors.ts b/apps/meteor/app/apps/server/converters/visitors.ts index 91722dfdd3f7e..9cb25dd50dab4 100644 --- a/apps/meteor/app/apps/server/converters/visitors.ts +++ b/apps/meteor/app/apps/server/converters/visitors.ts @@ -59,8 +59,8 @@ export class AppVisitorsConverter implements IAppVisitorsConverter { return undefined; } - const newVisitor: Partial = { - _id: visitor.id!, + const newVisitor: ILivechatVisitor = { + _id: visitor.id!, // FIXME not sure why id is optional in the Apps model username: visitor.username, name: visitor.name, token: visitor.token, @@ -69,8 +69,10 @@ export class AppVisitorsConverter implements IAppVisitorsConverter { status: (visitor.status as UserStatus | undefined) || UserStatus.ONLINE, ...(visitor.visitorEmails && { visitorEmails: visitor.visitorEmails }), ...(visitor.department && { department: visitor.department }), + ts: undefined!, // FIXME this field is required but has no equivalent in the Apps model + _updatedAt: visitor.updatedAt!, // FIXME might be fixed in ILivechatVisitor definition }; - return Object.assign(newVisitor, (visitor as { _unmappedProperties?: any })._unmappedProperties); + return Object.assign(newVisitor, (visitor as { _unmappedProperties?: Record })._unmappedProperties); } } From fa4d2e5bba33b0e3b99ee63a114acca1092c40af Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:10:24 -0300 Subject: [PATCH 73/80] refactor: review over `apps/meteor/app/lib/server/oauth/facebook.ts` --- apps/meteor/app/lib/server/oauth/facebook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/oauth/facebook.ts b/apps/meteor/app/lib/server/oauth/facebook.ts index b8a2f6cd5fe19..eca55d9702ea8 100644 --- a/apps/meteor/app/lib/server/oauth/facebook.ts +++ b/apps/meteor/app/lib/server/oauth/facebook.ts @@ -55,7 +55,7 @@ registerAccessTokenService('facebook', async (options) => { const fields: Record = {}; for (const key of whitelisted) { - if (identity[key]) { + if (Object.prototype.hasOwnProperty.call(identity, key)) { fields[key] = identity[key]; } } From 7dcc50cc2b195e6b4a04bad9f6d4dc5132409a8b Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:13:00 -0300 Subject: [PATCH 74/80] refactor: review over `apps/meteor/app/apps/server/converters/departments.ts` --- apps/meteor/app/apps/server/converters/departments.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/apps/server/converters/departments.ts b/apps/meteor/app/apps/server/converters/departments.ts index 0a465f4fba263..20bd30c23b988 100644 --- a/apps/meteor/app/apps/server/converters/departments.ts +++ b/apps/meteor/app/apps/server/converters/departments.ts @@ -58,15 +58,15 @@ export class AppDepartmentsConverter implements IAppDepartmentsConverter { const newDepartment: ILivechatDepartment = { _id: department.id, - name: department.name ?? '', - email: department.email ?? '', + name: department.name!, + email: department.email!, _updatedAt: department.updatedAt, enabled: department.enabled, numAgents: department.numberOfAgents, showOnOfflineForm: department.showOnOfflineForm, showOnRegistration: department.showOnRegistration, description: department.description, - offlineMessageChannelName: department.offlineMessageChannelName ?? '', + offlineMessageChannelName: department.offlineMessageChannelName!, requestTagBeforeClosingChat: department.requestTagBeforeClosingChat, chatClosingTags: department.chatClosingTags, abandonedRoomsCloseCustomMessage: department.abandonedRoomsCloseCustomMessage, From 5a27e0f07c8f50959e9d125880bc49ad5a86c038 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:16:17 -0300 Subject: [PATCH 75/80] refactor: review over `apps/meteor/app/lib/server/functions/validateCustomFields.ts` --- apps/meteor/app/lib/server/functions/validateCustomFields.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/functions/validateCustomFields.ts b/apps/meteor/app/lib/server/functions/validateCustomFields.ts index d623fff61ad48..5b7d399b9d386 100644 --- a/apps/meteor/app/lib/server/functions/validateCustomFields.ts +++ b/apps/meteor/app/lib/server/functions/validateCustomFields.ts @@ -37,7 +37,7 @@ export const validateCustomFields = function (fields: Record): void throw new Meteor.Error('error-user-registration-custom-field', `Field ${fieldName} is required`, { method: 'registerUser' }); } - if (field.type === 'select' && field.options?.indexOf(fields[fieldName]) === -1) { + if (field.type === 'select' && (!field.options || field.options.indexOf(fields[fieldName]) === -1)) { throw new Meteor.Error('error-user-registration-custom-field', `Value for field ${fieldName} is invalid`, { method: 'registerUser' }); } From 21fc7d1b18d56b5471b39c69807221f1eaa846ca Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:17:01 -0300 Subject: [PATCH 76/80] refactor: review over `apps/meteor/app/lib/server/oauth/google.ts` --- apps/meteor/app/lib/server/oauth/google.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/oauth/google.ts b/apps/meteor/app/lib/server/oauth/google.ts index 33aebdbc0ef6e..b7c988dea8e48 100644 --- a/apps/meteor/app/lib/server/oauth/google.ts +++ b/apps/meteor/app/lib/server/oauth/google.ts @@ -64,7 +64,7 @@ registerAccessTokenService('google', async (options) => { const fields: Record = {}; const { whitelistedFields } = Google as any; for (const key of whitelistedFields) { - if (identity[key]) { + if (Object.prototype.hasOwnProperty.call(identity, key)) { fields[key] = identity[key]; } } From 05e5b5af4df0bd894eda4b2e75e1a4fcf6b0f94f Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:20:11 -0300 Subject: [PATCH 77/80] refactor: review over `apps/meteor/app/custom-oauth/server/transform_helpers.ts` --- apps/meteor/app/custom-oauth/server/transform_helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/custom-oauth/server/transform_helpers.ts b/apps/meteor/app/custom-oauth/server/transform_helpers.ts index 4d431bdbb4860..9b6c2d18fec69 100644 --- a/apps/meteor/app/custom-oauth/server/transform_helpers.ts +++ b/apps/meteor/app/custom-oauth/server/transform_helpers.ts @@ -168,8 +168,8 @@ export const getRegexpMatch = (formula: string, data: any): any => { return value; }; -const templateStringRegex = /{{((?:(?!}}).)+)}}/g; export const fromTemplate = (template: string, data: any): any => { + const templateStringRegex = /{{((?:(?!}}).)+)}}/g; if (!templateStringRegex.test(template)) { return getNestedValue(template, data); } From 16ed65220152b50c088358f8bf7a06453dc4152b Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:29:30 -0300 Subject: [PATCH 78/80] refactor: review over `apps/meteor/ee/lib/misc/determineFileType.ts` --- apps/meteor/ee/lib/misc/determineFileType.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/ee/lib/misc/determineFileType.ts b/apps/meteor/ee/lib/misc/determineFileType.ts index afdf290fa5592..c1a73d03a31d5 100644 --- a/apps/meteor/ee/lib/misc/determineFileType.ts +++ b/apps/meteor/ee/lib/misc/determineFileType.ts @@ -1,4 +1,4 @@ -import { fromBuffer } from 'file-type'; +import FileType from 'file-type'; import { mime as MIME } from '../../../app/utils/lib/mimeTypes'; @@ -9,7 +9,7 @@ export async function determineFileType(buffer: Buffer, name: string): Promise Date: Mon, 2 Feb 2026 09:38:40 -0300 Subject: [PATCH 79/80] refactor: review over `apps/meteor/server/configuration/accounts_meld.ts` --- apps/meteor/server/configuration/accounts_meld.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/server/configuration/accounts_meld.ts b/apps/meteor/server/configuration/accounts_meld.ts index 7db6887168e55..e1750e9373a32 100644 --- a/apps/meteor/server/configuration/accounts_meld.ts +++ b/apps/meteor/server/configuration/accounts_meld.ts @@ -34,7 +34,7 @@ export async function configureAccounts(): Promise { if (serviceName === 'meteor-developer') { if (Array.isArray(serviceData.emails)) { const primaryEmail = serviceData.emails.sort((a) => (a.primary === true ? -1 : 1)).filter((item) => item.verified === true)[0]; - serviceData.email = primaryEmail && primaryEmail.address; + serviceData.email = primaryEmail?.address; } } @@ -44,7 +44,7 @@ export async function configureAccounts(): Promise { if (serviceData.email) { const user = await Users.findOneByEmailAddress(serviceData.email); - if (user != null && (user.services as any)?.[serviceName]?.id !== serviceData.id) { + if (!!user && user.services?.[serviceName as keyof typeof user.services]?.id !== serviceData.id) { const findQuery = { address: serviceData.email, verified: true, From 90bade67b5700dda1b185707e2d1d69672c53f88 Mon Sep 17 00:00:00 2001 From: Tasso Date: Mon, 2 Feb 2026 09:50:16 -0300 Subject: [PATCH 80/80] refactor: review over `apps/meteor/app/lib/server/oauth/google.ts` --- apps/meteor/app/lib/server/oauth/google.ts | 2 +- .../definition/externals/meteor/google-oauth.d.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/lib/server/oauth/google.ts b/apps/meteor/app/lib/server/oauth/google.ts index b7c988dea8e48..6b0e161ead319 100644 --- a/apps/meteor/app/lib/server/oauth/google.ts +++ b/apps/meteor/app/lib/server/oauth/google.ts @@ -62,7 +62,7 @@ registerAccessTokenService('google', async (options) => { }; const fields: Record = {}; - const { whitelistedFields } = Google as any; + const { whitelistedFields } = Google; for (const key of whitelistedFields) { if (Object.prototype.hasOwnProperty.call(identity, key)) { fields[key] = identity[key]; diff --git a/apps/meteor/definition/externals/meteor/google-oauth.d.ts b/apps/meteor/definition/externals/meteor/google-oauth.d.ts index a15f3de64b7f5..a4d34514e8c28 100644 --- a/apps/meteor/definition/externals/meteor/google-oauth.d.ts +++ b/apps/meteor/definition/externals/meteor/google-oauth.d.ts @@ -1,3 +1,14 @@ declare module 'meteor/google-oauth' { - export const Google: any; + export const Google: { + readonly name: string; + readonly whitelistedFields: string[]; + requestCredential( + options: Meteor.LoginWithExternalServiceOptions | undefined, + credentialRequestCompleteCallback: (credentialTokenOrError?: string | Error) => void, + ): void; + signIn?( + options: Meteor.LoginWithExternalServiceOptions | undefined, + callback?: (error: LoginError | undefined, result?: unknown) => void, + ): void; + }; }