From f3e8b6b5082934c6d142df442ee42a5ce968c969 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 10 Sep 2024 18:59:26 +0000 Subject: [PATCH 01/57] Bump 6.12.1 --- .changeset/bump-patch-1725994766358.md | 5 +++++ yarn.lock | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/bump-patch-1725994766358.md diff --git a/.changeset/bump-patch-1725994766358.md b/.changeset/bump-patch-1725994766358.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1725994766358.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index d89ba8bce628..a4da870cdb2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8934,10 +8934,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 6.0.0-rc.6 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 - "@rocket.chat/ui-kit": 0.36.1-rc.0 - "@rocket.chat/ui-video-conf": 10.0.0-rc.6 + "@rocket.chat/ui-avatar": 6.0.0 + "@rocket.chat/ui-contexts": 10.0.0 + "@rocket.chat/ui-kit": 0.36.1 + "@rocket.chat/ui-video-conf": 10.0.0 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9021,8 +9021,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.29 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 10.0.0-rc.6 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-client": 10.0.0 + "@rocket.chat/ui-contexts": 10.0.0 katex: "*" react: "*" languageName: unknown @@ -10228,7 +10228,7 @@ __metadata: typescript: ~5.3.3 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-contexts": 10.0.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -10278,7 +10278,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-contexts": 10.0.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -10448,8 +10448,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 6.0.0-rc.6 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-avatar": 6.0.0 + "@rocket.chat/ui-contexts": 10.0.0 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10536,7 +10536,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 10.0.0-rc.6 + "@rocket.chat/ui-contexts": 10.0.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 3cbb9f625292aa03fd30b48e44bd2bb8b3aaf0d9 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:15:29 +0000 Subject: [PATCH 02/57] fix: message parser being slow to process very long messages with too many symbols (#33254) Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> --- .changeset/short-drinks-itch.md | 6 + packages/message-parser/src/grammar.pegjs | 126 ++++---- packages/message-parser/src/utils.ts | 36 ++- packages/message-parser/tests/abuse.test.ts | 268 ++++++++++++++++++ .../message-parser/tests/emphasis.test.ts | 62 ++++ packages/message-parser/tests/link.test.ts | 24 ++ 6 files changed, 453 insertions(+), 69 deletions(-) create mode 100644 .changeset/short-drinks-itch.md create mode 100644 packages/message-parser/tests/abuse.test.ts diff --git a/.changeset/short-drinks-itch.md b/.changeset/short-drinks-itch.md new file mode 100644 index 000000000000..ee57330ffc86 --- /dev/null +++ b/.changeset/short-drinks-itch.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/message-parser': patch +'@rocket.chat/peggy-loader': patch +--- + +Improved the performance of the message parser diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index 182653a9c664..a6cae97facbf 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -10,6 +10,7 @@ emoji, emojiUnicode, emoticon, + extractFirstResult, heading, image, inlineCode, @@ -33,6 +34,11 @@ unorderedList, timestamp, } = require('./utils'); + +let skipBold = false; +let skipItalic = false; +let skipStrikethrough = false; +let skipReferences = false; }} Start @@ -212,7 +218,7 @@ Inline = value:(InlineItem / Any)+ EndOfLine? { return reducePlainTexts(value); InlineItem = Whitespace / Timestamp - / References + / MaybeReferences / AutolinkedPhone / AutolinkedEmail / AutolinkedURL @@ -240,7 +246,7 @@ References = "[" title:LinkTitle* "](" href:LinkRef ")" { return title.length ? link(href, reducePlainTexts(title)) : link(href); } / "<" href:LinkRef "|" title:LinkTitle2 ">" { return link(href, [plain(title)]); } -LinkTitle = (Whitespace / EmphasisForReferences) / anyTitle:$(!("](" .) .) { return plain(anyTitle) } +LinkTitle = (Whitespace / Emphasis) / anyTitle:$(!("](" .) .) { return plain(anyTitle) } LinkTitle2 = $([\x20-\x3B\x3D\x3F-\x60\x61-\x7B\x7D-\xFF] / NonASCII)+ @@ -349,14 +355,7 @@ AutoLinkURLBody = !(Extra* (Whitespace / EndOfLine)) . * Emphasis * */ -Emphasis = Bold / Italic / Strikethrough - -/** - * - * Emphasis for References - * -*/ -EmphasisForReferences = BoldForReferences / ItalicForReferences / StrikethroughForReferences +Emphasis = MaybeBold / MaybeItalic / MaybeStrikethrough /** * @@ -365,6 +364,63 @@ EmphasisForReferences = BoldForReferences / ItalicForReferences / StrikethroughF * */ +// This rule is used inside expressions that have a JS code ensuring they always fail, +// Without any pattern to match, peggy will think the rule may end up succedding without consuming any input, which could cause infinite loops +// So this unreachable rule is added to them to satisfy peggy's requirement. +BlockedByJavascript = 'unreachable' + +MaybeBold + = result:( + & { + if (skipBold) { return false; } + skipBold = true; + return true; + } + ( + (text:Bold { skipBold = false; return text; }) + / (& { skipBold = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + +MaybeStrikethrough + = result:( + & { + if (skipStrikethrough) { return false; } + skipStrikethrough = true; + return true; + } + ( + (text:Strikethrough { skipStrikethrough = false; return text; }) + / (& { skipStrikethrough = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + +MaybeItalic + = result:( + & { + if (skipItalic) { return false; } + skipItalic = true; + return true; + } + ( + (text:Italic { skipItalic = false; return text; }) + / (& { skipItalic = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + +MaybeReferences + = result:( + & { + if (skipReferences) { return false; } + skipReferences = true; + return true; + } + ( + (text:References { skipReferences = false; return text; }) + / (& { skipReferences = false; return false; } BlockedByJavascript) + ) + ) { return extractFirstResult(result); } + /* Italic */ Italic = value:$([a-zA-Z0-9]+ [\x5F] [\x5F]?) { return plain(value); } @@ -384,11 +440,11 @@ ItalicContentItems = text:ItalicContentItem+ { return reducePlainTexts(text); } ItalicContentItem = Whitespace / InlineCode - / References + / MaybeReferences / UserMention / ChannelMention - / Bold - / Strikethrough + / MaybeBold + / MaybeStrikethrough / Emoji / Emoticon / AnyItalic @@ -399,52 +455,12 @@ Bold = [\x2A] [\x2A] @BoldContent [\x2A] [\x2A] / [\x2A] @BoldContent [\x2A] BoldContent = text:BoldContentItem+ { return bold(reducePlainTexts(text)); } -BoldContentItem = Whitespace / InlineCode / References / UserMention / ChannelMention / Italic / Strikethrough / Emoji / Emoticon / AnyBold / Line +BoldContentItem = Whitespace / InlineCode / MaybeReferences / UserMention / ChannelMention / MaybeItalic / MaybeStrikethrough / Emoji / Emoticon / AnyBold / Line /* Strike */ Strikethrough = [\x7E] [\x7E] @StrikethroughContent [\x7E] [\x7E] / [\x7E] @StrikethroughContent [\x7E] -StrikethroughContent = text:(Timestamp / InlineCode / Whitespace / References / UserMention / ChannelMention / Italic / Bold / Emoji / Emoticon / AnyStrike / Line)+ { - return strike(reducePlainTexts(text)); - } - -/* Italic for References */ -ItalicForReferences - = value:$([a-zA-Z0-9]+ [\x5F] [\x5F]?) { return plain(value); } - / [\x5F] [\x5F] i:ItalicContentItemsForReferences [\x5F] [\x5F] t:$[a-zA-Z0-9]+ { - return reducePlainTexts([plain('__'), ...i, plain('__'), plain(t)])[0]; - } - / [\x5F] i:ItalicContentItemsForReferences [\x5F] t:$[a-zA-Z]+ { - return reducePlainTexts([plain('_'), ...i, plain('_'), plain(t)])[0]; - } - / [\x5F] [\x5F] @ItalicContentForReferences [\x5F] [\x5F] - / [\x5F] @ItalicContentForReferences [\x5F] - -ItalicContentForReferences = text:ItalicContentItemsForReferences { return italic(text); } - -ItalicContentItemsForReferences = text:ItalicContentItemForReferences+ { return reducePlainTexts(text); } - -ItalicContentItemForReferences - = Whitespace - / UserMention - / ChannelMention - / BoldForReferences - / StrikethroughForReferences - / Emoji - / Emoticon - / AnyItalic - / Line - / InlineCode - -/* Bold for References */ -BoldForReferences = [\x2A] [\x2A] @BoldContentForReferences [\x2A] [\x2A] / [\x2A] @BoldContentForReferences [\x2A] - -BoldContentForReferences = text:(Whitespace / UserMention / ChannelMention / ItalicForReferences / StrikethroughForReferences / Emoji / Emoticon / AnyBold / Line / InlineCode)+ { return bold(reducePlainTexts(text)); } - -/* Strike for References */ -StrikethroughForReferences = [\x7E] [\x7E] @StrikethroughContentForReferences [\x7E] [\x7E] / [\x7E] @StrikethroughContentForReferences [\x7E] - -StrikethroughContentForReferences = text:(Whitespace / UserMention / ChannelMention / ItalicForReferences / BoldForReferences / Emoji / Emoticon / AnyStrike / Line / InlineCode)+ { +StrikethroughContent = text:(Timestamp / Whitespace / InlineCode / MaybeReferences / UserMention / ChannelMention / MaybeItalic / MaybeBold / Emoji / Emoticon / AnyStrike / Line)+ { return strike(reducePlainTexts(text)); } diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 1f684b56d6ed..6c5d605c5c7a 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -198,21 +198,19 @@ const joinEmoji = ( export const reducePlainTexts = ( values: Paragraph['value'] ): Paragraph['value'] => - values - .flatMap((item) => item) - .reduce((result, item, index, values) => { - const next = values[index + 1]; - const current = joinEmoji(item, values[index - 1], next); - const previous: Inlines = result[result.length - 1]; - - if (previous) { - if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { - previous.value += current.value; - return result; - } + values.flat().reduce((result, item, index, values) => { + const next = values[index + 1]; + const current = joinEmoji(item, values[index - 1], next); + const previous: Inlines = result[result.length - 1]; + + if (previous) { + if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { + previous.value += current.value; + return result; } - return [...result, current]; - }, [] as Paragraph['value']); + } + return [...result, current]; + }, [] as Paragraph['value']); export const lineBreak = (): LineBreak => ({ type: 'LINE_BREAK', value: undefined, @@ -249,3 +247,13 @@ export const timestamp = ( fallback: plain(``), }; }; + +export const extractFirstResult = ( + value: Types[keyof Types]['value'] +): Types[keyof Types]['value'] => { + if (typeof value !== 'object' || !Array.isArray(value)) { + return value; + } + + return value.filter((item) => item).shift() as Types[keyof Types]['value']; +}; diff --git a/packages/message-parser/tests/abuse.test.ts b/packages/message-parser/tests/abuse.test.ts new file mode 100644 index 000000000000..c280ee75b4a0 --- /dev/null +++ b/packages/message-parser/tests/abuse.test.ts @@ -0,0 +1,268 @@ +import { parse } from '../src'; +import { paragraph, plain, bold, italic, strike } from '../src/utils'; + +test.each([ + [ + `This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. , REPEATx2 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEAT x3 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEAT x4 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEATx 5 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. , REPEAT x6 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. this can go long for some time, repeat x7 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. ,repeat x8 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some.`, + [ + paragraph([ + plain( + 'This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&' + ), + bold([ + plain('()'), + italic([ + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + strike([ + plain( + `, from now on we repeat some. , REPEATx2 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + ]), + plain( + ', from now on we repeat some. REPEAT x3 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()' + ), + ]), + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + strike([ + plain( + ', from now on we repeat some. REPEAT x4 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()' + ), + italic([ + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. REPEATx 5 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()` + ), + ]), + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + ]), + plain( + `, from now on we repeat some. , REPEAT x6 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&` + ), + ]), + plain( + `()_+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + strike([ + plain( + `, from now on we repeat some. this can go long for some time, repeat x7 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()` + ), + italic([ + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok~, from now on we repeat some. ,repeat x8 This a message designed to stress test the message parser, trying to force several rules to stack at the same time !!@#$%^&*()` + ), + ]), + plain( + `+, overloading the symbols {}:"|<>?, some more text ,./;'\\[], numbers too 1234567890-= let it call s o s ok` + ), + ]), + plain(', from now on we repeat some.'), + ]), + ], + ], + [ + '**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__', + [ + paragraph([ + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + italic([bold([plain('_')])]), + bold([italic([plain('**')]), italic([plain('**')])]), + plain('__'), + ]), + ], + ], +])('parses %p', (input, output) => { + expect(parse(input)).toMatchObject(output); +}); diff --git a/packages/message-parser/tests/emphasis.test.ts b/packages/message-parser/tests/emphasis.test.ts index e8e72a5882f1..b035999204ce 100644 --- a/packages/message-parser/tests/emphasis.test.ts +++ b/packages/message-parser/tests/emphasis.test.ts @@ -185,6 +185,68 @@ test.each([ ]), ], ], + [ + '**bold ~~and strike~~** **not bold ~~but strike** ~~ not strike~~', + [ + paragraph([ + bold([plain('bold '), strike([plain('and strike')])]), + plain(' **not bold '), + strike([plain('but strike** ')]), + plain(' not strike~~'), + ]), + ], + ], + [ + '**bold** **another bold** ~~strike~~ ~~another strike~~ **bold ~~and strike~~** **not bold ~~but strike** ~~ not strike~~', + [ + paragraph([ + bold([plain('bold')]), + plain(' '), + bold([plain('another bold')]), + plain(' '), + strike([plain('strike')]), + plain(' '), + strike([plain('another strike')]), + plain(' '), + bold([plain('bold '), strike([plain('and strike')])]), + plain(' **not bold '), + strike([plain('but strike** ')]), + plain(' not strike~~'), + ]), + ], + ], + [ + 'some_snake_case_text and even_more', + [paragraph([plain('some_snake_case_text and even_more')])], + ], + [ + 'some_snake_case_text and some __italic__ text', + [ + paragraph([ + plain('some_snake_case_text and some '), + italic([plain('italic')]), + plain(' text'), + ]), + ], + ], + [ + 'some__double__snake__case__text and even_more', + [paragraph([plain('some__double__snake__case__text and even_more')])], + ], + [ + 'some__double__snake__case__text and some __italic__ text', + [ + paragraph([ + plain('some__double__snake__case__text and some '), + italic([plain('italic')]), + plain(' text'), + ]), + ], + ], + [ + 'something__ __and italic__', + [paragraph([plain('something__ '), italic([plain('and italic')])])], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); diff --git a/packages/message-parser/tests/link.test.ts b/packages/message-parser/tests/link.test.ts index 1e083fde5d70..fcf371474bf2 100644 --- a/packages/message-parser/tests/link.test.ts +++ b/packages/message-parser/tests/link.test.ts @@ -584,6 +584,30 @@ Text after line break`, ]), ], ], + [ + '[test **bold** and __italic__](https://rocket.chat)', + [ + paragraph([ + link('https://rocket.chat', [ + plain('test '), + bold([plain('bold')]), + plain(' and '), + italic([plain('italic')]), + ]), + ]), + ], + ], + [ + '[test **bold with __italic__**](https://rocket.chat)', + [ + paragraph([ + link('https://rocket.chat', [ + plain('test '), + bold([plain('bold with '), italic([plain('italic')])]), + ]), + ]), + ], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); From ed7398d93067cf92b48791b4ca5f20cb94c89131 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 19:34:56 +0000 Subject: [PATCH 03/57] fix: Allow to use the token from `room.v` when requesting transcript instead of finding visitor (#33242) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .changeset/four-cherries-kneel.md | 5 +++ .../app/livechat/server/lib/sendTranscript.ts | 17 +++---- apps/meteor/tests/data/livechat/rooms.ts | 4 +- .../end-to-end/api/livechat/11-livechat.ts | 21 +++++++++ .../server/lib/sendTranscript.spec.ts | 45 +++++++++++++------ 5 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 .changeset/four-cherries-kneel.md diff --git a/.changeset/four-cherries-kneel.md b/.changeset/four-cherries-kneel.md new file mode 100644 index 000000000000..095d5af0aa76 --- /dev/null +++ b/.changeset/four-cherries-kneel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Allow to use the token from `room.v` when requesting transcript instead of visitor token. Visitors may change their tokens at any time, rendering old conversations impossible to access for them (or for APIs depending on token) as the visitor token won't match the `room.v` token. diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index 74032121ee50..bc7c06e0eaae 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -3,12 +3,13 @@ import { type IUser, type MessageTypesValues, type IOmnichannelSystemMessage, + type ILivechatVisitor, isFileAttachment, isFileImageAttachment, } from '@rocket.chat/core-typings'; import colors from '@rocket.chat/fuselage-tokens/colors'; import { Logger } from '@rocket.chat/logger'; -import { LivechatRooms, LivechatVisitors, Messages, Uploads, Users } from '@rocket.chat/models'; +import { LivechatRooms, Messages, Uploads, Users } from '@rocket.chat/models'; import { check } from 'meteor/check'; import moment from 'moment-timezone'; @@ -41,16 +42,12 @@ export async function sendTranscript({ const room = await LivechatRooms.findOneById(rid); - const visitor = await LivechatVisitors.getVisitorByToken(token, { - projection: { _id: 1, token: 1, language: 1, username: 1, name: 1 }, - }); - - if (!visitor) { - throw new Error('error-invalid-token'); + const visitor = room?.v as ILivechatVisitor; + if (token !== visitor?.token) { + throw new Error('error-invalid-visitor'); } - // @ts-expect-error - Visitor typings should include language? - const userLanguage = visitor?.language || settings.get('Language') || 'en'; + const userLanguage = settings.get('Language') || 'en'; const timezone = getTimezone(user); logger.debug(`Transcript will be sent using ${timezone} as timezone`); @@ -59,7 +56,7 @@ export async function sendTranscript({ } // allow to only user to send transcripts from their own chats - if (room.t !== 'l' || !room.v || room.v.token !== token) { + if (room.t !== 'l') { throw new Error('error-invalid-room'); } diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index 9532fd4214ab..b5d89762c614 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -33,10 +33,10 @@ export const createLivechatRoom = async (visitorToken: string, extraRoomParams?: return response.body.room; }; -export const createVisitor = (department?: string, visitorName?: string): Promise => +export const createVisitor = (department?: string, visitorName?: string, customEmail?: string): Promise => new Promise((resolve, reject) => { const token = getRandomVisitorToken(); - const email = `${token}@${token}.com`; + const email = customEmail || `${token}@${token}.com`; const phone = `${Math.floor(Math.random() * 10000000000)}`; void request.get(api(`livechat/visitor/${token}`)).end((err: Error, res: DummyResponse) => { if (!err && res && res.body && res.body.visitor) { diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts index c07f7bcecc81..7ce582025538 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-livechat.ts @@ -283,6 +283,27 @@ describe('LIVECHAT - Utils', () => { .send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' }); expect(body).to.have.property('success', true); }); + it('should allow a visitor to get a transcript even if token changed by using an old token that matches room.v', async () => { + const visitor = await createVisitor(); + const room = await createLivechatRoom(visitor.token); + await closeOmnichannelRoom(room._id); + const visitor2 = await createVisitor(undefined, undefined, visitor.visitorEmails?.[0].address); + const room2 = await createLivechatRoom(visitor2.token); + await closeOmnichannelRoom(room2._id); + + expect(visitor.token !== visitor2.token).to.be.true; + const { body } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor.token, rid: room._id, email: 'visitor@notadomain.com' }); + expect(body).to.have.property('success', true); + + const { body: body2 } = await request + .post(api('livechat/transcript')) + .set(credentials) + .send({ token: visitor2.token, rid: room2._id, email: 'visitor@notadomain.com' }); + expect(body2).to.have.property('success', true); + }); }); describe('livechat/transcript/:rid', () => { diff --git a/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts b/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts index 64da050cfd88..ca39a64c21a9 100644 --- a/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts +++ b/apps/meteor/tests/unit/app/livechat/server/lib/sendTranscript.spec.ts @@ -6,9 +6,6 @@ const modelsMock = { LivechatRooms: { findOneById: sinon.stub(), }, - LivechatVisitors: { - getVisitorByToken: sinon.stub(), - }, Messages: { findLivechatClosingMessage: sinon.stub(), findVisibleByRoomIdNotContainingTypesBeforeTs: sinon.stub(), @@ -75,7 +72,6 @@ describe('Send transcript', () => { beforeEach(() => { checkMock.reset(); modelsMock.LivechatRooms.findOneById.reset(); - modelsMock.LivechatVisitors.getVisitorByToken.reset(); modelsMock.Messages.findLivechatClosingMessage.reset(); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.reset(); modelsMock.Users.findOneById.reset(); @@ -87,11 +83,9 @@ describe('Send transcript', () => { await expect(sendTranscript({})).to.be.rejectedWith(Error); }); it('should throw error when visitor not found', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves(null); await expect(sendTranscript({ rid: 'rid', email: 'email', logger: mockLogger })).to.be.rejectedWith(Error); }); it('should attempt to send an email when params are valid using default subject', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } }); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); tStub.returns('Conversation Transcript'); @@ -117,7 +111,6 @@ describe('Send transcript', () => { ).to.be.true; }); it('should use provided subject', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } }); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); @@ -143,7 +136,6 @@ describe('Send transcript', () => { ).to.be.true; }); it('should use subject from setting (when configured) when no subject provided', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token' } }); modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); mockSettingValues.Livechat_transcript_email_subject = 'A custom subject obtained from setting.get'; @@ -170,36 +162,63 @@ describe('Send transcript', () => { }); it('should fail if room provided is invalid', async () => { modelsMock.LivechatRooms.findOneById.resolves(null); - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); await expect(sendTranscript({ rid: 'rid', email: 'email', logger: mockLogger })).to.be.rejectedWith(Error); }); it('should fail if room provided is of different type', async () => { modelsMock.LivechatRooms.findOneById.resolves({ t: 'c' }); - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error); }); it('should fail if room is of valid type, but doesnt doesnt have `v` property', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l' }); await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error); }); it('should fail if room is of valid type, has `v` prop, but it doesnt contain `token`', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { otherProp: 'xxx' } }); await expect(sendTranscript({ rid: 'rid', email: 'email' })).to.be.rejectedWith(Error); }); it('should fail if room is of valid type, has `v.token`, but its different from the one on param (room from another visitor)', async () => { - modelsMock.LivechatVisitors.getVisitorByToken.resolves({ language: null }); modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'xxx' } }); await expect(sendTranscript({ rid: 'rid', email: 'email', token: 'xveasdf' })).to.be.rejectedWith(Error); }); + + it('should throw an error when token is not the one on room.v', async () => { + modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'xxx' } }); + + await expect(sendTranscript({ rid: 'rid', email: 'email', token: 'xveasdf' })).to.be.rejectedWith(Error); + }); + it('should work when token matches room.v', async () => { + modelsMock.LivechatRooms.findOneById.resolves({ t: 'l', v: { token: 'token-123' } }); + modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.resolves([]); + delete mockSettingValues.Livechat_transcript_email_subject; + tStub.returns('Conversation Transcript'); + + await sendTranscript({ + rid: 'rid', + token: 'token-123', + email: 'email', + user: { _id: 'x', name: 'x', utcOffset: '-6', username: 'x' }, + }); + + expect(getTimezoneMock.calledWith({ _id: 'x', name: 'x', utcOffset: '-6', username: 'x' })).to.be.true; + expect(modelsMock.Messages.findLivechatClosingMessage.calledWith('rid', { projection: { ts: 1 } })).to.be.true; + expect(modelsMock.Messages.findVisibleByRoomIdNotContainingTypesBeforeTs.called).to.be.true; + expect( + mailerMock.calledWith({ + to: 'email', + from: 'test@rocket.chat', + subject: 'Conversation Transcript', + replyTo: 'test@rocket.chat', + html: '

', + }), + ).to.be.true; + }); }); From 7cb6de00336dcec878edcb223e7bb6158a848565 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:06:20 +0200 Subject: [PATCH 04/57] fix: imported fixes (#33246) --- .changeset/many-rules-shout.md | 5 + .../server/functions/canDeleteMessage.ts | 28 +- .../app/otr/server/methods/updateOTRAck.ts | 38 +- .../tabs/AppDetails/AppDetails.tsx | 11 +- .../tabs/AppReleases/AppReleasesItem.tsx | 4 +- .../views/marketplace/lib/purifyOptions.ts | 50 ++ apps/meteor/server/models/raw/Rooms.ts | 4 + apps/meteor/tests/end-to-end/api/methods.ts | 454 ++++++++++++++++++ packages/gazzodown/package.json | 2 + .../gazzodown/src/emoji/EmojiRenderer.tsx | 11 +- packages/gazzodown/src/katex/KatexBlock.tsx | 1 + packages/gazzodown/src/katex/KatexElement.tsx | 1 + .../model-typings/src/models/IRoomsModel.ts | 1 + 13 files changed, 595 insertions(+), 15 deletions(-) create mode 100644 .changeset/many-rules-shout.md create mode 100644 apps/meteor/client/views/marketplace/lib/purifyOptions.ts diff --git a/.changeset/many-rules-shout.md b/.changeset/many-rules-shout.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/many-rules-shout.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 7cd953a52bb2..fea37fd1c2a5 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -1,7 +1,8 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUser, IRoom } from '@rocket.chat/core-typings'; import { Rooms } from '@rocket.chat/models'; import { getValue } from '../../../settings/server/raw'; +import { canAccessRoomAsync } from './canAccessRoom'; import { hasPermissionAsync } from './hasPermission'; const elapsedTime = (ts: Date): number => { @@ -13,6 +14,25 @@ export const canDeleteMessageAsync = async ( uid: string, { u, rid, ts }: { u: Pick; rid: string; ts: Date }, ): Promise => { + const room = await Rooms.findOneById>(rid, { + projection: { + _id: 1, + ro: 1, + unmuted: 1, + t: 1, + teamId: 1, + prid: 1, + }, + }); + + if (!room) { + return false; + } + + if (!(await canAccessRoomAsync(room, { _id: uid }))) { + return false; + } + const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid); if (forceDelete) { @@ -45,12 +65,6 @@ export const canDeleteMessageAsync = async ( } } - const room = await Rooms.findOneById(rid, { projection: { ro: 1, unmuted: 1 } }); - - if (!room) { - return false; - } - if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', rid))) { // Unless the user was manually unmuted if (u.username && !(room.unmuted || []).includes(u.username)) { diff --git a/apps/meteor/app/otr/server/methods/updateOTRAck.ts b/apps/meteor/app/otr/server/methods/updateOTRAck.ts index 4fbd182e9d27..64e5e97fa4e5 100644 --- a/apps/meteor/app/otr/server/methods/updateOTRAck.ts +++ b/apps/meteor/app/otr/server/methods/updateOTRAck.ts @@ -1,8 +1,12 @@ import { api } from '@rocket.chat/core-services'; import type { IOTRMessage } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; +import { Rooms } from '@rocket.chat/models'; +import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; + declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { @@ -11,10 +15,40 @@ declare module '@rocket.chat/ddp-client' { } Meteor.methods({ - updateOTRAck({ message, ack }) { - if (!Meteor.userId()) { + async updateOTRAck({ message, ack }) { + const uid = Meteor.userId(); + if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOTRAck' }); } + + check(ack, String); + check(message, { + _id: String, + rid: String, + msg: String, + t: String, + ts: Date, + u: { + _id: String, + username: String, + name: String, + }, + }); + + if (message?.t !== 'otr') { + throw new Meteor.Error('error-invalid-message', 'Invalid message type', { method: 'updateOTRAck' }); + } + + const room = await Rooms.findOneByIdAndType(message.rid, 'd', { projection: { t: 1, _id: 1, uids: 1 } }); + + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'updateOTRAck' }); + } + + if (!(await canAccessRoomAsync(room, { _id: uid })) || (room.uids && (!message.u._id || !room.uids.includes(message.u._id)))) { + throw new Meteor.Error('error-invalid-user', 'Invalid user, not in room', { method: 'updateOTRAck' }); + } + const acknowledgeMessage: IOTRMessage = { ...message, otrAck: ack }; void api.broadcast('otrAckUpdate', { roomId: message.rid, acknowledgeMessage }); }, diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx index 8d17f669db83..5f3e427b8391 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppDetails/AppDetails.tsx @@ -2,10 +2,12 @@ import { Box, Callout, Chip, Margins } from '@rocket.chat/fuselage'; import { ExternalLink } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import React from 'react'; import ScreenshotCarouselAnchor from '../../../components/ScreenshotCarouselAnchor'; import type { AppInfo } from '../../../definitions/AppInfo'; +import { purifyOptions } from '../../../lib/purifyOptions'; import AppDetailsAPIs from './AppDetailsAPIs'; import { normalizeUrl } from './normalizeUrl'; @@ -61,7 +63,14 @@ const AppDetails = ({ app }: AppDetailsProps) => { {t('Description')} - + diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx index bc27053ea1d2..974b6d148e61 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppReleases/AppReleasesItem.tsx @@ -1,9 +1,11 @@ import { Accordion, Box } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import React from 'react'; import { useTimeAgo } from '../../../../../hooks/useTimeAgo'; +import { purifyOptions } from '../../../lib/purifyOptions'; type IRelease = { version: string; @@ -36,7 +38,7 @@ const AppReleasesItem = ({ release, ...props }: ReleaseItemProps): ReactElement return ( {release.detailedChangelog?.rendered ? ( - + ) : ( {t('No_release_information_provided')} )} diff --git a/apps/meteor/client/views/marketplace/lib/purifyOptions.ts b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts new file mode 100644 index 000000000000..cef1a2c8c707 --- /dev/null +++ b/apps/meteor/client/views/marketplace/lib/purifyOptions.ts @@ -0,0 +1,50 @@ +export const purifyOptions = { + ALLOWED_TAGS: [ + 'b', + 'i', + 'em', + 'strong', + 'br', + 'p', + 'ul', + 'ol', + 'li', + 'article', + 'aside', + 'figure', + 'section', + 'summary', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hgroup', + 'div', + 'hr', + 'span', + 'wbr', + 'abbr', + 'acronym', + 'cite', + 'code', + 'dfn', + 'figcaption', + 'mark', + 's', + 'samp', + 'sub', + 'sup', + 'var', + 'time', + 'q', + 'del', + 'ins', + 'rp', + 'rt', + 'ruby', + 'bdi', + 'bdo', + ], +}; diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 96cd5a3a3acf..ec3bd6fe8d40 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -903,6 +903,10 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.findOne(query, options); } + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options: FindOptions = {}): Promise { + return this.findOne({ _id: roomId, t: type }, options); + } + setCallStatus(_id: IRoom['_id'], status: IRoom['callStatus']): Promise { const query: Filter = { _id, diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 197a71f8e8f2..08945994e438 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -2602,6 +2602,158 @@ describe('Meteor.methods', () => { updateSetting('Message_AllowEditing_BlockEditInMinutes', 0), ]); }); + + describe('message deletion when user is not part of the room', () => { + let ridTestRoom: IRoom['_id']; + let messageIdTestRoom: IMessage['_id']; + let testUser: TestUser; + let testUserCredentials: Credentials; + + before('create room, add new owner, and leave room', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + const channelName = `methods-test-channel-${Date.now()}`; + + await request + .post(api('groups.create')) + .set(testUserCredentials) + .send({ + name: channelName, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('group._id'); + expect(res.body).to.have.nested.property('group.name', channelName); + expect(res.body).to.have.nested.property('group.t', 'p'); + expect(res.body).to.have.nested.property('group.msgs', 0); + ridTestRoom = res.body.group._id; + }); + + await request + .post(methodCall('sendMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'sendMessage', + params: [ + { + _id: `${Date.now() + Math.random()}`, + rid: ridTestRoom, + msg: 'just a random test message', + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('result').that.is.an('object'); + messageIdTestRoom = data.result._id; + }); + + await request + .post(methodCall('addUsersToRoom')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'addUsersToRoom', + params: [ + { + rid: ridTestRoom, + users: ['rocket.cat'], + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + }); + + await request + .post(api('groups.addOwner')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + userId: 'rocket.cat', + }) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + + await request + .post(api('groups.leave')) + .set(testUserCredentials) + .send({ + roomId: ridTestRoom, + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + }); + }); + + it('should not delete a message if the user is no longer member of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + it('should not delete a message if the user was never part of the room', async () => { + await request + .post(methodCall('deleteMessage')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'deleteMessage', + params: [{ _id: messageIdTestRoom, rid: ridTestRoom }], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + expect(res.body).to.have.a.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('msg', 'result'); + expect(data).to.have.a.property('id', 'id'); + expect(data.error).to.have.a.property('error', 'error-action-not-allowed'); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'p', roomId: ridTestRoom }), deleteUser(testUser)])); + }); }); describe('[@setUserActiveStatus]', () => { @@ -3348,6 +3500,7 @@ describe('Meteor.methods', () => { .end(done); }); }); + (IS_EE ? describe : describe.skip)('[@auditGetAuditions] EE', () => { let testUser: TestUser; let testUserCredentials: Credentials; @@ -3451,4 +3604,305 @@ describe('Meteor.methods', () => { }); }); }); + + describe('UpdateOTRAck', () => { + let testUser: TestUser; + let testUser2: TestUser; + let testUserCredentials: Credentials; + let dmTestId: IRoom['_id']; + + before(async () => { + testUser = await createUser(); + testUser2 = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + + before('create direct conversation between both users', (done) => { + void request + .post(methodCall('createDirectMessage')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'createDirectMessage', + params: [testUser2.username], + id: 'id', + msg: 'method', + }), + }) + .end((_err, res) => { + const result = JSON.parse(res.body.message); + expect(result.result).to.be.an('object'); + expect(result.result).to.have.property('rid').that.is.an('string'); + + dmTestId = result.result.rid; + done(); + }); + }); + + after(() => Promise.all([deleteRoom({ type: 'd', roomId: dmTestId }), deleteUser(testUser), deleteUser(testUser2)])); + + it('should fail if required parameters are not present', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + // rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', "Match error: Missing key 'rid'"); + }); + }); + + it('should fail if required parameters have a different type', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: { $ne: 'test' }, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Match error: Expected string, got object in field rid'); + }); + }); + + it('should fail if "t" is not "otr"', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'notOTR', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid message type [error-invalid-message]'); + }); + }); + + it('should fail if room does not exist', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'test', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if room is not a DM', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: 'GENERAL', + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: 'test', + username: 'test', + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid room [error-invalid-room]'); + }); + }); + + it('should fail if user is not part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error'); + expect(data.error).to.have.a.property('message', 'Invalid user, not in room [error-invalid-user]'); + }); + }); + + it('should pass if all parameters are present and user is part of DM room', async () => { + await request + .post(methodCall('updateOTRAck')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'updateOTRAck', + params: [ + { + message: { + _id: 'czjFdkFab7H5bWxYq', + rid: dmTestId, + msg: 'test', + t: 'otr', + ts: { $date: 1725447664093 }, + u: { + _id: testUser._id, + username: testUser.username, + name: 'test', + }, + }, + ack: 'test', + }, + ], + id: '18', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('message'); + expect(res.body).to.have.a.property('success', true); + }); + }); + }); }); diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index d4a18fb09a76..f6c48c7f8e47 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -72,7 +72,9 @@ "react": "*" }, "dependencies": { + "@types/dompurify": "^3.0.5", "date-fns": "^3.3.1", + "dompurify": "^3.1.6", "highlight.js": "^11.5.1", "react-error-boundary": "^3.1.4" }, diff --git a/packages/gazzodown/src/emoji/EmojiRenderer.tsx b/packages/gazzodown/src/emoji/EmojiRenderer.tsx index 7a4ca5324930..84116361157c 100644 --- a/packages/gazzodown/src/emoji/EmojiRenderer.tsx +++ b/packages/gazzodown/src/emoji/EmojiRenderer.tsx @@ -1,5 +1,6 @@ import { MessageEmoji, ThreadMessageEmoji } from '@rocket.chat/fuselage'; import type * as MessageParser from '@rocket.chat/message-parser'; +import DOMPurify from 'dompurify'; import { ReactElement, useMemo, useContext, memo } from 'react'; import { MarkupInteractionContext } from '../MarkupInteractionContext'; @@ -14,10 +15,12 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): const fallback = useMemo(() => ('unicode' in emoji ? emoji.unicode : `:${emoji.shortCode ?? emoji.value.value}:`), [emoji]); + const sanitizedFallback = DOMPurify.sanitize(fallback); + const descriptors = useMemo(() => { - const detected = detectEmoji?.(fallback); + const detected = detectEmoji?.(sanitizedFallback); return detected?.length !== 0 ? detected : undefined; - }, [detectEmoji, fallback]); + }, [detectEmoji, sanitizedFallback]); return ( <> @@ -34,8 +37,8 @@ const EmojiRenderer = ({ big = false, preview = false, ...emoji }: EmojiProps): )} )) ?? ( - - {fallback} + + {sanitizedFallback} )} diff --git a/packages/gazzodown/src/katex/KatexBlock.tsx b/packages/gazzodown/src/katex/KatexBlock.tsx index 5913185d3969..267b310b3897 100644 --- a/packages/gazzodown/src/katex/KatexBlock.tsx +++ b/packages/gazzodown/src/katex/KatexBlock.tsx @@ -15,6 +15,7 @@ const KatexBlock = ({ code }: KatexBlockProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/gazzodown/src/katex/KatexElement.tsx b/packages/gazzodown/src/katex/KatexElement.tsx index 3595f698f7ae..099c2f82cf8c 100644 --- a/packages/gazzodown/src/katex/KatexElement.tsx +++ b/packages/gazzodown/src/katex/KatexElement.tsx @@ -15,6 +15,7 @@ const KatexElement = ({ code }: KatexElementProps): ReactElement => { macros: { '\\href': '\\@secondoftwo', }, + maxSize: 100, }), [code], ); diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 498a3c6b4bbc..9097a89c1413 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -192,6 +192,7 @@ export interface IRoomsModel extends IBaseModel { setE2eKeyId(roomId: string, e2eKeyId: string, options?: FindOptions): Promise; findOneByImportId(importId: string, options?: FindOptions): Promise; findOneByNameAndNotId(name: string, rid: string): Promise; + findOneByIdAndType(roomId: IRoom['_id'], type: IRoom['t'], options?: FindOptions): Promise; findOneByDisplayName(displayName: string, options?: FindOptions): Promise; findOneByNameAndType( name: string, From c2d7216c1c50254f028456f175675d909084b5ab Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:25:17 +0000 Subject: [PATCH 05/57] fix: Retention Policy cached settings not updated during upgrade procedure (#33265) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/pink-swans-teach.md | 5 +++++ apps/meteor/server/startup/migrations/xrun.ts | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/pink-swans-teach.md diff --git a/.changeset/pink-swans-teach.md b/.changeset/pink-swans-teach.md new file mode 100644 index 000000000000..7c85572a78d5 --- /dev/null +++ b/.changeset/pink-swans-teach.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fixed retention policy max age settings not being respected after upgrade diff --git a/apps/meteor/server/startup/migrations/xrun.ts b/apps/meteor/server/startup/migrations/xrun.ts index 7f6c8a68ad55..0344649f9993 100644 --- a/apps/meteor/server/startup/migrations/xrun.ts +++ b/apps/meteor/server/startup/migrations/xrun.ts @@ -2,6 +2,7 @@ import { Settings } from '@rocket.chat/models'; import type { UpdateResult } from 'mongodb'; import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; +import { settings } from '../../../app/settings/server'; import { migrateDatabase, onServerVersionChange } from '../../lib/migrations'; import { ensureCloudWorkspaceRegistered } from '../cloudRegistration'; @@ -23,10 +24,13 @@ const moveRetentionSetting = async () => { { _id: { $in: Array.from(maxAgeSettingMap.keys()) }, value: { $ne: -1 } }, { projection: { _id: 1, value: 1 } }, ).forEach(({ _id, value }) => { - if (!maxAgeSettingMap.has(_id)) { + const newSettingId = maxAgeSettingMap.get(_id); + if (!newSettingId) { throw new Error(`moveRetentionSetting - Setting ${_id} equivalent does not exist`); } + const newValue = convertDaysToMs(Number(value)); + promises.push( Settings.updateOne( { @@ -34,11 +38,17 @@ const moveRetentionSetting = async () => { }, { $set: { - value: convertDaysToMs(Number(value)), + value: newValue, }, }, ), ); + + const currentCache = settings.getSetting(newSettingId); + if (!currentCache) { + return; + } + settings.set({ ...currentCache, value: newValue }); }); await Promise.all(promises); From ec6f6a650a552f39dec4af757ebefec3dbe54782 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 12 Sep 2024 15:05:00 -0300 Subject: [PATCH 06/57] chore: moved UserAutoComplete to ui-client package (#33249) --- apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx | 2 +- .../client/views/omnichannel/agents/AgentsTable/AddAgent.tsx | 2 +- apps/meteor/client/views/omnichannel/managers/AddManager.tsx | 2 +- .../components/UserAutoComplete/UserAutoComplete.stories.tsx | 1 - .../src}/components/UserAutoComplete/UserAutoComplete.tsx | 2 +- .../ui-client/src}/components/UserAutoComplete/index.ts | 0 packages/ui-client/src/components/index.ts | 1 + 7 files changed, 5 insertions(+), 5 deletions(-) rename {apps/meteor/client => packages/ui-client/src}/components/UserAutoComplete/UserAutoComplete.stories.tsx (94%) rename {apps/meteor/client => packages/ui-client/src}/components/UserAutoComplete/UserAutoComplete.tsx (97%) rename {apps/meteor/client => packages/ui-client/src}/components/UserAutoComplete/index.ts (100%) diff --git a/apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx b/apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx index 62adc2a0405f..76a95cf4a0e7 100644 --- a/apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx +++ b/apps/meteor/client/omnichannel/monitors/MonitorsTable.tsx @@ -13,6 +13,7 @@ import { StatesAction, } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { UserAutoComplete } from '@rocket.chat/ui-client'; import { useTranslation, useToastMessageDispatch, useMethod, useEndpoint, useSetModal } from '@rocket.chat/ui-contexts'; import { useMutation, useQuery, hashQueryKey } from '@tanstack/react-query'; import React, { useMemo, useState } from 'react'; @@ -31,7 +32,6 @@ import { } from '../../components/GenericTable'; import { usePagination } from '../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../components/GenericTable/hooks/useSort'; -import UserAutoComplete from '../../components/UserAutoComplete'; import { queryClient } from '../../lib/queryClient'; const MonitorsTable = () => { diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx index 47d60b3d9958..4ad019281bbf 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsTable/AddAgent.tsx @@ -1,10 +1,10 @@ import { Button, Box, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { UserAutoComplete } from '@rocket.chat/ui-client'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import UserAutoComplete from '../../../../components/UserAutoComplete'; import { useEndpointAction } from '../../../../hooks/useEndpointAction'; type AddAgentProps = { diff --git a/apps/meteor/client/views/omnichannel/managers/AddManager.tsx b/apps/meteor/client/views/omnichannel/managers/AddManager.tsx index e21896eef9fa..24817a03846e 100644 --- a/apps/meteor/client/views/omnichannel/managers/AddManager.tsx +++ b/apps/meteor/client/views/omnichannel/managers/AddManager.tsx @@ -1,10 +1,10 @@ import { Button, Box, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { UserAutoComplete } from '@rocket.chat/ui-client'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; -import UserAutoComplete from '../../../components/UserAutoComplete'; import { useEndpointAction } from '../../../hooks/useEndpointAction'; const AddManager = ({ reload }: { reload: () => void }): ReactElement => { diff --git a/apps/meteor/client/components/UserAutoComplete/UserAutoComplete.stories.tsx b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.stories.tsx similarity index 94% rename from apps/meteor/client/components/UserAutoComplete/UserAutoComplete.stories.tsx rename to packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.stories.tsx index 757c10b35b86..b9baa3174dda 100644 --- a/apps/meteor/client/components/UserAutoComplete/UserAutoComplete.stories.tsx +++ b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.stories.tsx @@ -1,5 +1,4 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import React from 'react'; import UserAutoComplete from '.'; diff --git a/apps/meteor/client/components/UserAutoComplete/UserAutoComplete.tsx b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx similarity index 97% rename from apps/meteor/client/components/UserAutoComplete/UserAutoComplete.tsx rename to packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx index 3f61f421035c..72a8eecec4d0 100644 --- a/apps/meteor/client/components/UserAutoComplete/UserAutoComplete.tsx +++ b/packages/ui-client/src/components/UserAutoComplete/UserAutoComplete.tsx @@ -4,7 +4,7 @@ import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ComponentProps, ReactElement } from 'react'; -import React, { memo, useMemo, useState } from 'react'; +import { memo, useMemo, useState } from 'react'; const query = ( term = '', diff --git a/apps/meteor/client/components/UserAutoComplete/index.ts b/packages/ui-client/src/components/UserAutoComplete/index.ts similarity index 100% rename from apps/meteor/client/components/UserAutoComplete/index.ts rename to packages/ui-client/src/components/UserAutoComplete/index.ts diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 1eccad1fd234..f78c32427cdb 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -13,3 +13,4 @@ export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; export * from './FeaturePreview/FeaturePreview'; export * from './RoomBanner'; +export { default as UserAutoComplete } from './UserAutoComplete'; From 811e2cb5762b126f6b7cb4709f822c80fd75bf54 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 12 Sep 2024 16:24:15 -0300 Subject: [PATCH 07/57] chore: move GenericMenu to ui-client package (#33247) --- .../NavBarPagesToolbar/NavBarItemAuditMenu.tsx | 2 +- .../NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx | 2 +- .../NavBarPagesToolbar/hooks/useAuditMenu.tsx | 2 +- .../NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx | 2 +- .../NavBarItemAdministrationMenu.tsx | 2 +- .../NavBarSettingsToolbar/UserMenu/UserMenu.tsx | 5 ++--- .../UserMenu/hooks/useAccountItems.tsx | 3 +-- .../UserMenu/hooks/useStatusItems.tsx | 2 +- .../UserMenu/hooks/useUserMenu.tsx | 2 +- .../hooks/useAdministrationMenu.tsx | 3 +-- .../components/message/toolbar/MessageActionMenu.tsx | 3 +-- apps/meteor/client/hooks/useAppActionButtons.ts | 2 +- .../navbar/actions/NavbarAdministrationAction.tsx | 3 +-- .../client/navbar/actions/NavbarAuditAction.tsx | 3 +-- .../navbar/actions/NavbarMarketplaceAction.tsx | 3 +-- apps/meteor/client/sidebar/header/UserMenu.tsx | 12 ++++++++---- .../client/sidebar/header/actions/Administration.tsx | 2 +- .../client/sidebar/header/actions/CreateRoom.tsx | 2 +- apps/meteor/client/sidebar/header/actions/Sort.tsx | 2 +- .../header/actions/hooks/useAdministrationItems.tsx | 3 +-- .../header/actions/hooks/useAdministrationMenu.tsx | 2 +- .../sidebar/header/actions/hooks/useAppsItems.tsx | 2 +- .../sidebar/header/actions/hooks/useAuditItems.tsx | 2 +- .../header/actions/hooks/useCreateRoomItems.tsx | 2 +- .../header/actions/hooks/useGroupingListItems.tsx | 3 +-- .../actions/hooks/useMatrixFederationItems.tsx.tsx | 2 +- .../header/actions/hooks/useSortModeItems.tsx | 2 +- .../header/actions/hooks/useViewModeItems.tsx | 3 +-- .../client/sidebar/header/hooks/useAccountItems.tsx | 3 +-- .../client/sidebar/header/hooks/useStatusItems.tsx | 2 +- .../client/sidebar/header/hooks/useUserMenu.tsx | 2 +- .../client/sidebarv2/header/actions/CreateRoom.tsx | 2 +- apps/meteor/client/sidebarv2/header/actions/Sort.tsx | 2 +- .../header/actions/hooks/useCreateRoomItems.tsx | 2 +- .../header/actions/hooks/useGroupingListItems.tsx | 3 +-- .../header/actions/hooks/useMatrixFederationItems.ts | 2 +- .../header/actions/hooks/useSortModeItems.tsx | 2 +- .../header/actions/hooks/useViewModeItems.tsx | 3 +-- .../views/admin/moderation/MessageContextFooter.tsx | 2 +- .../admin/moderation/ModerationConsoleActions.tsx | 2 +- .../moderation/UserReports/ModConsoleUserActions.tsx | 2 +- .../moderation/UserReports/UserContextFooter.tsx | 2 +- .../moderation/hooks/useDeactivateUserAction.tsx | 2 +- .../moderation/hooks/useDeleteMessagesAction.tsx | 2 +- .../admin/moderation/hooks/useDismissUserAction.tsx | 2 +- .../admin/moderation/hooks/useResetAvatarAction.tsx | 2 +- apps/meteor/client/views/marketplace/AppMenu.tsx | 2 +- .../views/room/Header/RoomToolbox/RoomToolbox.tsx | 4 ++-- .../views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx | 4 ++-- .../client/views/room/UserCard/UserCardWithData.tsx | 2 +- .../MessageBoxActionsToolbar.tsx | 4 ++-- .../hooks/useAudioMessageAction.ts | 2 +- .../hooks/useCreateDiscussionAction.tsx | 2 +- .../hooks/useFileUploadAction.ts | 2 +- .../hooks/useShareLocationAction.tsx | 2 +- .../hooks/useVideoMessageAction.ts | 2 +- .../hooks/useWebdavActions.tsx | 2 +- .../FormattingToolbarDropdown.tsx | 4 ++-- .../room/contextualBar/Info/RoomInfo/RoomInfo.tsx | 2 +- .../contextualBar/RoomMembers/RoomMembersActions.tsx | 2 +- .../room/contextualBar/UserInfo/UserInfoActions.tsx | 2 +- .../hooks/useUserInfoActions/useUserInfoActions.ts | 2 +- .../contextualBar/channels/TeamsChannelItemMenu.tsx | 4 ++-- .../src}/components/GenericMenu/GenericMenu.spec.tsx | 1 - .../src}/components/GenericMenu/GenericMenu.tsx | 2 +- .../src}/components/GenericMenu/GenericMenuItem.tsx | 1 - .../GenericMenu/hooks/useHandleMenuAction.tsx | 5 ++--- .../ui-client/src/components/GenericMenu/index.ts | 3 +++ packages/ui-client/src/components/index.ts | 1 + 69 files changed, 83 insertions(+), 91 deletions(-) rename {apps/meteor/client => packages/ui-client/src}/components/GenericMenu/GenericMenu.spec.tsx (98%) rename {apps/meteor/client => packages/ui-client/src}/components/GenericMenu/GenericMenu.tsx (99%) rename {apps/meteor/client => packages/ui-client/src}/components/GenericMenu/GenericMenuItem.tsx (97%) rename {apps/meteor/client => packages/ui-client/src}/components/GenericMenu/hooks/useHandleMenuAction.tsx (81%) create mode 100644 packages/ui-client/src/components/GenericMenu/index.ts diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx index 07936f6f4276..7c8a50338e7d 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemAuditMenu.tsx @@ -1,9 +1,9 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; import { useAuditMenu } from './hooks/useAuditMenu'; type NavBarItemAuditMenuProps = Omit, 'is'>; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx index 4a2bbc916b57..85687bb12a2e 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/NavBarItemMarketPlaceMenu.tsx @@ -1,9 +1,9 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; import { useMarketPlaceMenu } from './hooks/useMarketPlaceMenu'; type NavBarItemMarketPlaceMenuProps = Omit, 'is'>; diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx index 88a2a5de31aa..97c8d7299497 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useAuditMenu.tsx @@ -1,6 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { usePermission, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; export const useAuditMenu = () => { diff --git a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx index fd704ffafe1f..034ab0367e81 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesToolbar/hooks/useMarketPlaceMenu.tsx @@ -1,8 +1,8 @@ import { Badge, Skeleton } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, usePermission, useRouter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; import { useUserDropdownAppsActionButtons } from '../../../hooks/useAppActionButtons'; import { useAppRequestStats } from '../../../views/marketplace/hooks/useAppRequestStats'; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx index 045b36425512..8236eec030e8 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/NavBarItemAdministrationMenu.tsx @@ -1,9 +1,9 @@ import { NavBarItem } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useCurrentRoutePath, useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; import { useAdministrationMenu } from './hooks/useAdministrationMenu'; type NavBarItemAdministrationMenuProps = Omit, 'is'>; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx index 531ff8a74b66..22895d55388f 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx @@ -1,11 +1,10 @@ import type { IUser } from '@rocket.chat/core-typings'; +import { GenericMenu, useHandleMenuAction } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo, useState } from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; -import { useHandleMenuAction } from '../../../components/GenericMenu/hooks/useHandleMenuAction'; import UserMenuButton from './UserMenuButton'; import { useUserMenu } from './hooks/useUserMenu'; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx index bf1b7e55f244..82c39c5c1b10 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx @@ -1,11 +1,10 @@ import { Badge } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; - export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx index 2957d22c5e32..1c9cf09e4610 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useStatusItems.tsx @@ -1,11 +1,11 @@ import { Box } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { callbacks } from '../../../../../lib/callbacks'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import MarkdownText from '../../../../components/MarkdownText'; import { UserStatus } from '../../../../components/UserStatus'; import { userStatuses } from '../../../../lib/userStatuses'; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx index a969c853d797..85a481f3e257 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useUserMenu.tsx @@ -1,9 +1,9 @@ import type { IUser } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useLogout, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import UserMenuHeader from '../UserMenuHeader'; import { useAccountItems } from './useAccountItems'; import { useStatusItems } from './useStatusItems'; diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx index 54d4818128ea..e3c4a358c7c1 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/hooks/useAdministrationMenu.tsx @@ -1,7 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useAtLeastOnePermission, usePermission, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; - const ADMIN_PERMISSIONS = [ 'view-statistics', 'run-import', diff --git a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx index 6c35f7b73dbd..155599c5b51d 100644 --- a/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageActionMenu.tsx @@ -1,11 +1,10 @@ import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { MouseEvent, ReactElement } from 'react'; import React from 'react'; import type { MessageActionConditionProps, MessageActionConfig } from '../../../../app/ui-utils/client/lib/MessageAction'; -import GenericMenu from '../../GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../GenericMenu/GenericMenuItem'; type MessageActionConfigOption = Omit & { action: (e?: MouseEvent) => void; diff --git a/apps/meteor/client/hooks/useAppActionButtons.ts b/apps/meteor/client/hooks/useAppActionButtons.ts index 5ee20f7772bf..64c46f370a1f 100644 --- a/apps/meteor/client/hooks/useAppActionButtons.ts +++ b/apps/meteor/client/hooks/useAppActionButtons.ts @@ -1,5 +1,6 @@ import type { IUIActionButton, UIActionButtonContext } from '@rocket.chat/apps-engine/definition/ui'; import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useStream, useToastMessageDispatch, useUserId } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -10,7 +11,6 @@ import { UiKitTriggerTimeoutError } from '../../app/ui-message/client/UiKitTrigg import type { MessageActionConfig, MessageActionContext } from '../../app/ui-utils/client/lib/MessageAction'; import type { MessageBoxAction } from '../../app/ui-utils/client/lib/messageBox'; import { Utilities } from '../../ee/lib/misc/Utilities'; -import type { GenericMenuItemProps } from '../components/GenericMenu/GenericMenuItem'; import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; import { useApplyButtonFilters, useApplyButtonAuthFilter } from './useApplyButtonFilters'; diff --git a/apps/meteor/client/navbar/actions/NavbarAdministrationAction.tsx b/apps/meteor/client/navbar/actions/NavbarAdministrationAction.tsx index 3431b9e928ad..f3c34c333dc5 100644 --- a/apps/meteor/client/navbar/actions/NavbarAdministrationAction.tsx +++ b/apps/meteor/client/navbar/actions/NavbarAdministrationAction.tsx @@ -1,9 +1,8 @@ +import { GenericMenu, useHandleMenuAction } from '@rocket.chat/ui-client'; import { useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; -import { useHandleMenuAction } from '../../components/GenericMenu/hooks/useHandleMenuAction'; import { NavbarAction } from '../../components/Navbar'; import { useAdministrationItems } from '../../sidebar/header/actions/hooks/useAdministrationItems'; diff --git a/apps/meteor/client/navbar/actions/NavbarAuditAction.tsx b/apps/meteor/client/navbar/actions/NavbarAuditAction.tsx index fc613be29b99..7e679faf0ab2 100644 --- a/apps/meteor/client/navbar/actions/NavbarAuditAction.tsx +++ b/apps/meteor/client/navbar/actions/NavbarAuditAction.tsx @@ -1,9 +1,8 @@ +import { GenericMenu, useHandleMenuAction } from '@rocket.chat/ui-client'; import { useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; -import { useHandleMenuAction } from '../../components/GenericMenu/hooks/useHandleMenuAction'; import { NavbarAction } from '../../components/Navbar'; import { useAuditItems } from '../../sidebar/header/actions/hooks/useAuditItems'; diff --git a/apps/meteor/client/navbar/actions/NavbarMarketplaceAction.tsx b/apps/meteor/client/navbar/actions/NavbarMarketplaceAction.tsx index 8f54b8260b2b..92e3eccf592a 100644 --- a/apps/meteor/client/navbar/actions/NavbarMarketplaceAction.tsx +++ b/apps/meteor/client/navbar/actions/NavbarMarketplaceAction.tsx @@ -1,10 +1,9 @@ import { IconButton } from '@rocket.chat/fuselage'; +import { GenericMenu, useHandleMenuAction } from '@rocket.chat/ui-client'; import { useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import type { AllHTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; -import { useHandleMenuAction } from '../../components/GenericMenu/hooks/useHandleMenuAction'; import { NavbarAction } from '../../components/Navbar'; import { useAppsItems } from '../../sidebar/header/actions/hooks/useAppsItems'; diff --git a/apps/meteor/client/sidebar/header/UserMenu.tsx b/apps/meteor/client/sidebar/header/UserMenu.tsx index 592fc19656e9..b106caa20b16 100644 --- a/apps/meteor/client/sidebar/header/UserMenu.tsx +++ b/apps/meteor/client/sidebar/header/UserMenu.tsx @@ -1,11 +1,15 @@ import type { IUser } from '@rocket.chat/core-typings'; -import { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from '@rocket.chat/ui-client'; +import { + FeaturePreview, + FeaturePreviewOn, + FeaturePreviewOff, + GenericMenu, + useHandleMenuAction, + type GenericMenuItemProps, +} from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useState, memo } from 'react'; -import GenericMenu from '../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../components/GenericMenu/GenericMenuItem'; -import { useHandleMenuAction } from '../../components/GenericMenu/hooks/useHandleMenuAction'; import UserAvatarWithStatus from './UserAvatarWithStatus'; import UserAvatarWithStatusUnstable from './UserAvatarWithStatusUnstable'; import { useUserMenu } from './hooks/useUserMenu'; diff --git a/apps/meteor/client/sidebar/header/actions/Administration.tsx b/apps/meteor/client/sidebar/header/actions/Administration.tsx index fbed4afec4cb..a0016db7e75c 100644 --- a/apps/meteor/client/sidebar/header/actions/Administration.tsx +++ b/apps/meteor/client/sidebar/header/actions/Administration.tsx @@ -1,9 +1,9 @@ import { Sidebar } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import { useAdministrationMenu } from './hooks/useAdministrationMenu'; type AdministrationProps = Omit, 'is'>; diff --git a/apps/meteor/client/sidebar/header/actions/CreateRoom.tsx b/apps/meteor/client/sidebar/header/actions/CreateRoom.tsx index 478e7cce33e1..3392db4ed917 100644 --- a/apps/meteor/client/sidebar/header/actions/CreateRoom.tsx +++ b/apps/meteor/client/sidebar/header/actions/CreateRoom.tsx @@ -1,9 +1,9 @@ import { Sidebar } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import { useCreateRoom } from './hooks/useCreateRoomMenu'; type CreateRoomProps = Omit, 'is'>; diff --git a/apps/meteor/client/sidebar/header/actions/Sort.tsx b/apps/meteor/client/sidebar/header/actions/Sort.tsx index e7f3b398e5f6..1fe4b947c661 100644 --- a/apps/meteor/client/sidebar/header/actions/Sort.tsx +++ b/apps/meteor/client/sidebar/header/actions/Sort.tsx @@ -1,9 +1,9 @@ import { Sidebar } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import { useSortMenu } from './hooks/useSortMenu'; type SortProps = Omit, 'is'>; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.tsx index 5e802c2d1851..9c74bacd23ce 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationItems.tsx @@ -1,7 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useRoute, useRouter, useAtLeastOnePermission, usePermission } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; - const ADMIN_PERMISSIONS = [ 'view-statistics', 'run-import', diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationMenu.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationMenu.tsx index 5be021cf0b7e..cf71cf4223b9 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationMenu.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAdministrationMenu.tsx @@ -1,6 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { useAdministrationItems } from './useAdministrationItems'; import { useAppsItems } from './useAppsItems'; import { useAuditItems } from './useAuditItems'; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx index 3717f33fb195..f7b9cea0d56a 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAppsItems.tsx @@ -1,8 +1,8 @@ import { Badge, Skeleton } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useRoute, usePermission } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { useUserDropdownAppsActionButtons } from '../../../../hooks/useAppActionButtons'; import { useAppRequestStats } from '../../../../views/marketplace/hooks/useAppRequestStats'; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx index 8e2640711347..cf53f1ffef03 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useAuditItems.tsx @@ -1,6 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useRoute, usePermission } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { useHasLicenseModule } from '../../../../hooks/useHasLicenseModule'; /** diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx index 7b2770a60ab5..1bc64d1ba8d1 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useCreateRoomItems.tsx @@ -1,7 +1,7 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useSetting, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; import CreateDiscussion from '../../../../components/CreateDiscussion'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import CreateChannelWithData from '../../CreateChannel'; import CreateDirectMessage from '../../CreateDirectMessage'; import CreateTeam from '../../CreateTeam'; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx index 646b85c838be..5dcb53f2ad2f 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useGroupingListItems.tsx @@ -1,9 +1,8 @@ import { CheckBox } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; - export const useGroupingListItems = (): GenericMenuItemProps[] => { const t = useTranslation(); diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useMatrixFederationItems.tsx.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useMatrixFederationItems.tsx.tsx index b3ac63f13773..7e628378749d 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useMatrixFederationItems.tsx.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useMatrixFederationItems.tsx.tsx @@ -1,6 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import MatrixFederationSearch from '../../MatrixFederationSearch'; import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx index 56041ab4e571..0e2bf818a0bf 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useSortModeItems.tsx @@ -1,8 +1,8 @@ import { RadioButton } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { OmnichannelSortingDisclaimer, useOmnichannelSortingDisclaimer, diff --git a/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx index ca2855d09db5..a33230a15729 100644 --- a/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx +++ b/apps/meteor/client/sidebar/header/actions/hooks/useViewModeItems.tsx @@ -1,9 +1,8 @@ import { RadioButton, ToggleSwitch } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; - export const useViewModeItems = (): GenericMenuItemProps[] => { const t = useTranslation(); diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index 1f18d826843d..2be6b2b1dea2 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -1,11 +1,10 @@ import { Badge } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; - export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); diff --git a/apps/meteor/client/sidebar/header/hooks/useStatusItems.tsx b/apps/meteor/client/sidebar/header/hooks/useStatusItems.tsx index 026f7c80400e..dc3fa2a3b277 100644 --- a/apps/meteor/client/sidebar/header/hooks/useStatusItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useStatusItems.tsx @@ -1,11 +1,11 @@ import { Box } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import React, { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { callbacks } from '../../../../lib/callbacks'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; import MarkdownText from '../../../components/MarkdownText'; import { UserStatus } from '../../../components/UserStatus'; import { userStatuses } from '../../../lib/userStatuses'; diff --git a/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx b/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx index de43d30306e9..c0c6f94a4ed8 100644 --- a/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useUserMenu.tsx @@ -1,9 +1,9 @@ import type { IUser } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useLogout, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../components/GenericMenu/GenericMenuItem'; import UserMenuHeader from '../UserMenuHeader'; import { useAccountItems } from './useAccountItems'; import { useStatusItems } from './useStatusItems'; diff --git a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx b/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx index a80954c8b297..f12de10f19f9 100644 --- a/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/CreateRoom.tsx @@ -1,9 +1,9 @@ import { SidebarV2Action } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import { useCreateRoom } from './hooks/useCreateRoomMenu'; type CreateRoomProps = Omit, 'is'>; diff --git a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx b/apps/meteor/client/sidebarv2/header/actions/Sort.tsx index 9956acf266b5..d5394c28c6e6 100644 --- a/apps/meteor/client/sidebarv2/header/actions/Sort.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/Sort.tsx @@ -1,9 +1,9 @@ import { SidebarV2Action } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { HTMLAttributes } from 'react'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import { useSortMenu } from './hooks/useSortMenu'; type SortProps = Omit, 'is'>; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx index 3935ad0039df..4d313975d6f5 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useCreateRoomItems.tsx @@ -1,7 +1,7 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useSetting, useAtLeastOnePermission } from '@rocket.chat/ui-contexts'; import CreateDiscussion from '../../../../components/CreateDiscussion'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import CreateChannelModal from '../../CreateChannelModal'; import CreateDirectMessage from '../../CreateDirectMessage'; import CreateTeamModal from '../../CreateTeamModal'; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx index 646b85c838be..5dcb53f2ad2f 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useGroupingListItems.tsx @@ -1,9 +1,8 @@ import { CheckBox } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; - export const useGroupingListItems = (): GenericMenuItemProps[] => { const t = useTranslation(); diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts b/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts index b3ac63f13773..7e628378749d 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useMatrixFederationItems.ts @@ -1,6 +1,6 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import MatrixFederationSearch from '../../MatrixFederationSearch'; import { useCreateRoomModal } from '../../hooks/useCreateRoomModal'; diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx index 56041ab4e571..0e2bf818a0bf 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useSortModeItems.tsx @@ -1,8 +1,8 @@ import { RadioButton } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { OmnichannelSortingDisclaimer, useOmnichannelSortingDisclaimer, diff --git a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx index ca2855d09db5..a33230a15729 100644 --- a/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx +++ b/apps/meteor/client/sidebarv2/header/actions/hooks/useViewModeItems.tsx @@ -1,9 +1,8 @@ import { RadioButton, ToggleSwitch } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useUserPreference, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; - export const useViewModeItems = (): GenericMenuItemProps[] => { const t = useTranslation(); diff --git a/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx b/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx index 1cbe3c7c121b..878b4d84f507 100644 --- a/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx +++ b/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx @@ -1,8 +1,8 @@ import { Button, ButtonGroup, Box } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import useDeactivateUserAction from './hooks/useDeactivateUserAction'; import useDeleteMessagesAction from './hooks/useDeleteMessagesAction'; import useDismissUserAction from './hooks/useDismissUserAction'; diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx index 53615ad0321c..85d081e652da 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx @@ -1,8 +1,8 @@ // import { Menu, Option } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import type { ModerationConsoleRowProps } from './ModerationConsoleTableRow'; import useDeactivateUserAction from './hooks/useDeactivateUserAction'; import useDeleteMessagesAction from './hooks/useDeleteMessagesAction'; diff --git a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx index eb1ec183fcfe..13da70b48da2 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/ModConsoleUserActions.tsx @@ -1,7 +1,7 @@ +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; import useDeactivateUserAction from '../hooks/useDeactivateUserAction'; import useDismissUserAction from '../hooks/useDismissUserAction'; import useResetAvatarAction from '../hooks/useResetAvatarAction'; diff --git a/apps/meteor/client/views/admin/moderation/UserReports/UserContextFooter.tsx b/apps/meteor/client/views/admin/moderation/UserReports/UserContextFooter.tsx index 601e403e7e74..c437746ad8ef 100644 --- a/apps/meteor/client/views/admin/moderation/UserReports/UserContextFooter.tsx +++ b/apps/meteor/client/views/admin/moderation/UserReports/UserContextFooter.tsx @@ -1,8 +1,8 @@ import { Button, ButtonGroup, Box } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; import useDeactivateUserAction from '../hooks/useDeactivateUserAction'; import useDismissUserAction from '../hooks/useDismissUserAction'; import useResetAvatarAction from '../hooks/useResetAvatarAction'; diff --git a/apps/meteor/client/views/admin/moderation/hooks/useDeactivateUserAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useDeactivateUserAction.tsx index 2ac0eeffe131..ac046fe80633 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useDeactivateUserAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useDeactivateUserAction.tsx @@ -1,9 +1,9 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useRouteParameter, useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import GenericModal from '../../../../components/GenericModal'; const useDeactivateUserAction = (userId: string, isUserReport?: boolean): GenericMenuItemProps => { diff --git a/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx index 4d5d5690492d..3fb22dfd0c5e 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx @@ -1,8 +1,8 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useRouteParameter, useRouter, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import GenericModal from '../../../../components/GenericModal'; const useDeleteMessagesAction = (userId: string): GenericMenuItemProps => { diff --git a/apps/meteor/client/views/admin/moderation/hooks/useDismissUserAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useDismissUserAction.tsx index 09a30358d737..72e5dc03fd95 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useDismissUserAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useDismissUserAction.tsx @@ -1,9 +1,9 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useRouter, useSetModal, useToastMessageDispatch, useRouteParameter } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import GenericModal from '../../../../components/GenericModal'; const useDismissUserAction = (userId: string, isUserReport?: boolean): GenericMenuItemProps => { diff --git a/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx b/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx index 377aad97b186..8e488234814b 100644 --- a/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx +++ b/apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx @@ -1,8 +1,8 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useEndpoint, useSetModal, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import GenericModal from '../../../../components/GenericModal'; const useResetAvatarAction = (userId: string): GenericMenuItemProps => { diff --git a/apps/meteor/client/views/marketplace/AppMenu.tsx b/apps/meteor/client/views/marketplace/AppMenu.tsx index be1159ebd4e2..a1d3942ee304 100644 --- a/apps/meteor/client/views/marketplace/AppMenu.tsx +++ b/apps/meteor/client/views/marketplace/AppMenu.tsx @@ -1,9 +1,9 @@ import type { App } from '@rocket.chat/core-typings'; import { MenuItem, MenuItemContent, MenuSection, MenuV2, Skeleton } from '@rocket.chat/fuselage'; +import { useHandleMenuAction } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; -import { useHandleMenuAction } from '../../components/GenericMenu/hooks/useHandleMenuAction'; import type { AppMenuOption } from './hooks/useAppMenu'; import { useAppMenu } from './hooks/useAppMenu'; diff --git a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx index bdda6f33f0e3..ff9880ba1e84 100644 --- a/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/Header/RoomToolbox/RoomToolbox.tsx @@ -1,11 +1,11 @@ import type { Box } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo } from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; diff --git a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx index 5fda368711c1..abf9b41b89da 100644 --- a/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx +++ b/apps/meteor/client/views/room/HeaderV2/RoomToolbox/RoomToolbox.tsx @@ -1,11 +1,11 @@ import type { Box } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo } from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { HeaderToolbarAction, HeaderToolbarDivider } from '../../../../components/Header'; import { useRoomToolbox } from '../../contexts/RoomToolboxContext'; import type { RoomToolboxActionConfig } from '../../contexts/RoomToolboxContext'; diff --git a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx index a12e9a143cc2..a26b5f31e77d 100644 --- a/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx +++ b/apps/meteor/client/views/room/UserCard/UserCardWithData.tsx @@ -1,11 +1,11 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useSetting, useRolesDescription, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; import { getUserDisplayName } from '../../../../lib/getUserDisplayName'; -import GenericMenu from '../../../components/GenericMenu/GenericMenu'; import LocalTime from '../../../components/LocalTime'; import { UserCard, UserCardAction, UserCardRole, UserCardSkeleton } from '../../../components/UserCard'; import { ReactiveUserStatus } from '../../../components/UserStatus'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 90216f159069..38396844866b 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,5 +1,7 @@ import type { IRoom, IMessage } from '@rocket.chat/core-typings'; import type { Icon } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; @@ -8,8 +10,6 @@ import React, { memo } from 'react'; import { messageBox } from '../../../../../../app/ui-utils/client'; import { isTruthy } from '../../../../../../lib/isTruthy'; -import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem'; import { useMessageboxAppsActionButtons } from '../../../../../hooks/useAppActionButtons'; import { useChat } from '../../../contexts/ChatContext'; import { useRoom } from '../../../contexts/RoomContext'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts index 58cd7b72189b..9738f714fa97 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useAudioMessageAction.ts @@ -1,9 +1,9 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useEffect, useMemo } from 'react'; import { AudioRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/AudioRecorder'; -import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx index 50702f6887aa..03bff48876f3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx @@ -1,10 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useSetting, usePermission, useSetModal } from '@rocket.chat/ui-contexts'; import React from 'react'; import CreateDiscussion from '../../../../../../components/CreateDiscussion'; -import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; export const useCreateDiscussionAction = (room?: IRoom): GenericMenuItemProps => { const t = useTranslation(); diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts index f911b2b63b1f..f576aba9803c 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useFileUploadAction.ts @@ -1,7 +1,7 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useFileInput } from '../../../../../../hooks/useFileInput'; import { useChat } from '../../../../contexts/ChatContext'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx index bf44496b0d67..a49253c2744d 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useShareLocationAction.tsx @@ -1,9 +1,9 @@ import type { IMessage, IRoom } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useSetting, useSetModal, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import ShareLocationModal from '../../../../ShareLocation/ShareLocationModal'; export const useShareLocationAction = (room?: IRoom, tmid?: IMessage['tmid']): GenericMenuItemProps => { diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts index cd7e2c5af6fc..2608597fb202 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useVideoMessageAction.ts @@ -1,9 +1,9 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import { useEffect, useMemo } from 'react'; import { VideoRecorder } from '../../../../../../../app/ui/client/lib/recorderjs/videoRecorder'; -import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useChat } from '../../../../contexts/ChatContext'; import { useMediaActionTitle } from '../../hooks/useMediaActionTitle'; import { useMediaPermissions } from '../../hooks/useMediaPermissions'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx index ae80a7407851..806bebaa65e3 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useWebdavActions.tsx @@ -1,9 +1,9 @@ import type { IWebdavAccountIntegration } from '@rocket.chat/core-typings'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useSetModal, useSetting } from '@rocket.chat/ui-contexts'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import type { GenericMenuItemProps } from '../../../../../../components/GenericMenu/GenericMenuItem'; import { useWebDAVAccountIntegrationsQuery } from '../../../../../../hooks/webdav/useWebDAVAccountIntegrationsQuery'; import { useChat } from '../../../../contexts/ChatContext'; import AddWebdavAccountModal from '../../../../webdav/AddWebdavAccountModal'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx index b2ba79792e04..d39b74c079a2 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxFormattingToolbar/FormattingToolbarDropdown.tsx @@ -1,9 +1,9 @@ +import { GenericMenu } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { isPromptButton, type FormattingButton } from '../../../../../../app/ui-message/client/messageBox/messageBoxFormatting'; -import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../../../../components/GenericMenu/GenericMenuItem'; import type { ComposerAPI } from '../../../../../lib/chats/ChatAPI'; type FormattingToolbarDropdownProps = { diff --git a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx index b0bf1d083a71..31f96a344677 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.tsx @@ -1,6 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Box, Callout, IconButton } from '@rocket.chat/fuselage'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -12,7 +13,6 @@ import { ContextualbarClose, ContextualbarTitle, } from '../../../../../components/Contextualbar'; -import GenericMenu from '../../../../../components/GenericMenu/GenericMenu'; import { InfoPanel, InfoPanelActionGroup, diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx index 7df7c468bb01..c265a28cb284 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx @@ -1,9 +1,9 @@ import type { IUser, IRoom } from '@rocket.chat/core-typings'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; import { useUserInfoActions } from '../../hooks/useUserInfoActions'; type RoomMembersActionsProps = { diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx index 0b4f30fb1f29..17e8bd1470ba 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx @@ -1,11 +1,11 @@ /* eslint-disable react/display-name, react/no-multi-comp */ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { ButtonGroup, IconButton, Skeleton } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; import { UserInfoAction } from '../../../../components/UserInfo'; import { useMemberExists } from '../../../hooks/useMemberExists'; import { useUserInfoActions } from '../../hooks/useUserInfoActions'; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index 2a70ff55ee34..c04df6b7521a 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -1,10 +1,10 @@ import type { IRoom, IUser } from '@rocket.chat/core-typings'; import type { Icon } from '@rocket.chat/fuselage'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { useMemo } from 'react'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { useEmbeddedLayout } from '../../../../hooks/useEmbeddedLayout'; import { useAddUserAction } from './actions/useAddUserAction'; import { useBlockUserAction } from './actions/useBlockUserAction'; diff --git a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx index 97b1bdceb670..418883869a4a 100644 --- a/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx +++ b/apps/meteor/client/views/teams/contextualBar/channels/TeamsChannelItemMenu.tsx @@ -1,10 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { CheckBox } from '@rocket.chat/fuselage'; +import { GenericMenu } from '@rocket.chat/ui-client'; +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; -import type { GenericMenuItemProps } from '../../../../components/GenericMenu/GenericMenuItem'; import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom'; import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam'; import { useToggleAutoJoin } from './hooks/useToggleAutoJoin'; diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx b/packages/ui-client/src/components/GenericMenu/GenericMenu.spec.tsx similarity index 98% rename from apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx rename to packages/ui-client/src/components/GenericMenu/GenericMenu.spec.tsx index 530bd1404dc7..bdd7f5fbda18 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenu.spec.tsx +++ b/packages/ui-client/src/components/GenericMenu/GenericMenu.spec.tsx @@ -1,6 +1,5 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import React from 'react'; import GenericMenu from './GenericMenu'; diff --git a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx b/packages/ui-client/src/components/GenericMenu/GenericMenu.tsx similarity index 99% rename from apps/meteor/client/components/GenericMenu/GenericMenu.tsx rename to packages/ui-client/src/components/GenericMenu/GenericMenu.tsx index 294a98770523..3f65df59a6ca 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenu.tsx +++ b/packages/ui-client/src/components/GenericMenu/GenericMenu.tsx @@ -1,7 +1,6 @@ import { IconButton, MenuItem, MenuSection, MenuV2 } from '@rocket.chat/fuselage'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactNode } from 'react'; -import React from 'react'; import type { GenericMenuItemProps } from './GenericMenuItem'; import GenericMenuItem from './GenericMenuItem'; @@ -13,6 +12,7 @@ type GenericMenuCommonProps = { disabled?: boolean; callbackAction?: () => void; }; + type GenericMenuConditionalProps = | { sections?: { diff --git a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx b/packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx similarity index 97% rename from apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx rename to packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx index c01a64d708a0..41576a2a40a6 100644 --- a/apps/meteor/client/components/GenericMenu/GenericMenuItem.tsx +++ b/packages/ui-client/src/components/GenericMenu/GenericMenuItem.tsx @@ -1,6 +1,5 @@ import { MenuItemColumn, MenuItemContent, MenuItemIcon, MenuItemInput } from '@rocket.chat/fuselage'; import type { ComponentProps, MouseEvent, ReactNode } from 'react'; -import React from 'react'; export type GenericMenuItemProps = { id: string; diff --git a/apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx b/packages/ui-client/src/components/GenericMenu/hooks/useHandleMenuAction.tsx similarity index 81% rename from apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx rename to packages/ui-client/src/components/GenericMenu/hooks/useHandleMenuAction.tsx index dd1115765194..fb423643b7c6 100644 --- a/apps/meteor/client/components/GenericMenu/hooks/useHandleMenuAction.tsx +++ b/packages/ui-client/src/components/GenericMenu/hooks/useHandleMenuAction.tsx @@ -2,12 +2,11 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import type { GenericMenuItemProps } from '../GenericMenuItem'; -export const useHandleMenuAction = (items: GenericMenuItemProps[], callbackAction?: () => void) => { - return useEffectEvent((id) => { +export const useHandleMenuAction = (items: GenericMenuItemProps[], callbackAction?: () => void) => + useEffectEvent((id) => { const item = items.find((item) => item.id === id && !!item.onClick); if (item) { item.onClick?.(); callbackAction?.(); } }); -}; diff --git a/packages/ui-client/src/components/GenericMenu/index.ts b/packages/ui-client/src/components/GenericMenu/index.ts new file mode 100644 index 000000000000..a0753e281481 --- /dev/null +++ b/packages/ui-client/src/components/GenericMenu/index.ts @@ -0,0 +1,3 @@ +export { default as GenericMenu } from './GenericMenu'; +export { default as GenericMenuItem, GenericMenuItemProps } from './GenericMenuItem'; +export { useHandleMenuAction } from './hooks/useHandleMenuAction'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index f78c32427cdb..8642983229aa 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -14,3 +14,4 @@ export * from './MultiSelectCustom/MultiSelectCustom'; export * from './FeaturePreview/FeaturePreview'; export * from './RoomBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; +export * from './GenericMenu'; From 98b6a209013f2469deb1f405e30791c9481b145b Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 13 Sep 2024 09:01:28 -0300 Subject: [PATCH 08/57] refactor(i18n): Increase the adoption of `react-i18next` - Phase 1 (#33175) --- .../react-i18next-npm-15.0.1-0812bb73aa.patch | 99 +++++ apps/meteor/app/2fa/server/code/EmailCheck.ts | 2 +- .../app/2fa/server/functions/resetTOTP.ts | 2 +- .../app/lib/server/methods/addUsersToRoom.ts | 13 +- .../app/lib/server/methods/sendMessage.ts | 5 +- .../server/startup/mentionUserNotInChannel.ts | 8 +- .../modals/PlaceChatOnHoldModal.tsx | 4 +- .../meteor/app/livechat/server/api/v1/room.ts | 2 +- apps/meteor/app/livechat/server/lib/Helper.ts | 4 +- .../app/livechat/server/lib/QueueManager.ts | 4 +- .../messageBox/AddLinkComposerActionModal.tsx | 4 +- apps/meteor/app/utils/lib/i18n.ts | 3 +- .../NavBarItemOmniChannelCallDialPad.tsx | 4 +- .../NavBarItemOmnichannelCallToggleError.tsx | 4 +- ...NavBarItemOmnichannelCallToggleLoading.tsx | 4 +- .../NavBarItemOmnichannelCallToggleReady.tsx | 4 +- .../UserMenu/UserMenu.tsx | 4 +- .../apps/gameCenter/GameCenterContainer.tsx | 4 +- .../GameCenterInvitePlayersModal.tsx | 4 +- .../components/ActionManagerBusyState.tsx | 4 +- .../components/AutoCompleteDepartment.tsx | 4 +- .../AutoCompleteDepartmentMultiple.tsx | 4 +- .../components/ConfirmOwnerChangeModal.tsx | 4 +- .../Contextualbar/ContextualbarBack.tsx | 4 +- .../Contextualbar/ContextualbarClose.tsx | 4 +- .../meteor/client/components/FilterByText.tsx | 4 +- .../components/FingerprintChangeModal.tsx | 4 +- .../FingerprintChangeModalConfirmation.tsx | 4 +- .../components/GenericError/GenericError.tsx | 4 +- .../components/GenericModal/GenericModal.tsx | 4 +- .../GenericNoResults/GenericNoResults.tsx | 4 +- .../hooks/useItemsPerPageLabel.ts | 4 +- .../hooks/useShowingResultsLabel.ts | 10 +- .../GenericUpsellModal/GenericUpsellModal.tsx | 4 +- .../components/ImageGallery/ImageGallery.tsx | 4 +- .../ImageGallery/ImageGalleryError.tsx | 4 +- .../ImageGallery/ImageGalleryLoading.tsx | 4 +- .../InfoPanel/RetentionPolicyCallout.tsx | 4 +- apps/meteor/client/components/LocalTime.tsx | 4 +- .../meteor/client/components/MarkdownText.tsx | 4 +- .../modals/ReturnChatQueueModal.tsx | 4 +- .../Omnichannel/modals/TranscriptModal.tsx | 4 +- .../client/components/Sidebar/Header.tsx | 4 +- .../Sidebar/SidebarItemsAssembler.tsx | 6 +- .../SidebarToggler/SidebarTogglerButton.tsx | 4 +- apps/meteor/client/components/TextCopy.tsx | 4 +- .../TwoFactorModal/TwoFactorPasswordModal.tsx | 4 +- .../TwoFactorModal/TwoFactorTotpModal.tsx | 4 +- .../client/components/UrlChangeModal.tsx | 9 +- .../client/components/UserCard/UserCard.tsx | 4 +- .../client/components/UserInfo/UserInfo.tsx | 4 +- .../meteor/client/components/WarningModal.tsx | 4 +- .../components/dashboards/PeriodSelector.tsx | 6 +- .../client/components/dashboards/periods.ts | 23 +- .../components/dashboards/usePeriodLabel.ts | 6 +- .../DeviceManagementTable.tsx | 4 +- .../deviceManagement/LoggedOutBanner.tsx | 4 +- .../components/message/IgnoredContent.tsx | 4 +- .../components/message/MessageHeader.tsx | 4 +- .../message/ReadReceiptIndicator.tsx | 4 +- .../message/content/BroadcastMetrics.tsx | 4 +- .../message/content/DiscussionMetrics.tsx | 4 +- .../components/message/content/Reactions.tsx | 4 +- .../message/content/actions/MessageAction.tsx | 4 +- .../structure/AttachmentDownloadBase.tsx | 4 +- .../attachments/structure/image/Load.tsx | 4 +- .../attachments/structure/image/Retry.tsx | 4 +- .../collapsible/CollapsibleContent.tsx | 4 +- .../content/location/MapViewFallback.tsx | 4 +- .../message/content/location/MapViewImage.tsx | 4 +- .../content/reactions/ReactionTooltip.tsx | 4 +- .../content/urlPreviews/OEmbedCollapsible.tsx | 4 +- .../message/header/MessageRoles.tsx | 4 +- .../notification/MessageNotification.tsx | 4 +- .../message/toolbar/MessageActionMenu.tsx | 4 +- .../message/variants/SystemMessage.tsx | 4 +- .../message/variants/ThreadMessagePreview.tsx | 4 +- .../ThreadMessagePreviewBody.tsx | 4 +- .../components/voip/room/VoipRoomForeword.tsx | 4 +- .../client/hooks/useDecryptedMessage.ts | 4 +- .../hooks/useDownloadFromServiceWorker.ts | 3 +- apps/meteor/client/hooks/useFormatDuration.ts | 4 +- .../meteor/client/hooks/useHighlightedCode.ts | 4 +- .../additionalForms/BusinessHoursMultiple.tsx | 4 +- .../additionalForms/ContactManager.js | 4 +- .../CustomFieldsAdditionalForm.tsx | 4 +- .../additionalForms/DepartmentForwarding.tsx | 4 +- .../additionalForms/MaxChatsPerAgent.tsx | 4 +- .../MaxChatsPerAgentDisplay.tsx | 4 +- .../additionalForms/PrioritiesSelect.tsx | 4 +- .../CannedResponseEditWithDepartmentData.tsx | 4 +- .../cannedResponses/CannedResponseFilter.tsx | 4 +- .../InsertPlaceholderDropdown.tsx | 4 +- .../CannedResponse/CannedResponse.tsx | 4 +- .../CannedResponse/CannedResponseList.tsx | 4 +- .../contextualBar/CannedResponse/Item.tsx | 4 +- .../components/RoomActivityIcon/index.tsx | 4 +- .../client/omnichannel/hooks/useScopeDict.ts | 4 +- .../omnichannel/monitors/MonitorsPage.tsx | 4 +- .../priorities/PrioritiesResetModal.tsx | 4 +- .../priorities/PrioritiesTable.tsx | 4 +- .../priorities/PriorityEditFormWithData.tsx | 4 +- .../omnichannel/priorities/PriorityIcon.tsx | 4 +- .../omnichannel/priorities/PriorityList.tsx | 4 +- .../reports/components/AgentsTable.tsx | 4 +- .../components/ReportCardEmptyState.tsx | 4 +- .../components/ReportCardErrorState.tsx | 4 +- .../reports/hooks/useAgentsSection.tsx | 5 +- .../reports/hooks/useChannelsSection.tsx | 12 +- .../reports/hooks/useDefaultDownload.tsx | 4 +- .../reports/hooks/useDepartmentsSection.tsx | 5 +- .../reports/hooks/useStatusSection.tsx | 10 +- .../reports/hooks/useTagsSection.tsx | 5 +- .../reports/sections/AgentsSection.tsx | 4 +- .../reports/utils/formatPeriodDescription.tsx | 6 +- .../tags/AutoCompleteTagsMultiple.tsx | 4 +- .../sidebar/RoomList/RoomListWrapper.tsx | 4 +- .../sidebar/footer/SidebarFooterWatermark.tsx | 4 +- .../client/sidebar/footer/voip/VoipFooter.tsx | 4 +- .../FederatedRoomListEmptyPlaceholder.tsx | 4 +- .../FederatedRoomListErrorBoundary.tsx | 4 +- .../FederatedRoomListItem.tsx | 4 +- .../MatrixFederationSearch.tsx | 4 +- .../meteor/client/sidebar/header/UserMenu.tsx | 4 +- .../sidebar/header/actions/Administration.tsx | 4 +- .../sidebar/header/actions/CreateRoom.tsx | 4 +- .../client/sidebar/header/actions/Sort.tsx | 4 +- .../actions/hooks/useAdministrationMenu.tsx | 4 +- .../hooks/useMatrixFederationItems.tsx.tsx | 4 +- .../header/actions/hooks/useSortMenu.tsx | 4 +- .../sidebar/sections/OverMacLimitSection.tsx | 4 +- .../sections/StatusDisabledSection.tsx | 4 +- .../actions/OmnichannelCallDialPad.tsx | 4 +- .../actions/OmnichannelCallToggleError.tsx | 4 +- .../actions/OmnichannelCallToggleLoading.tsx | 4 +- .../actions/OmnichannelCallToggleReady.tsx | 4 +- .../sidebarv2/RoomList/RoomListWrapper.tsx | 4 +- .../footer/SidebarFooterWatermark.tsx | 4 +- .../sidebarv2/footer/voip/VoipFooter.tsx | 4 +- .../FederatedRoomListEmptyPlaceholder.tsx | 4 +- .../FederatedRoomListErrorBoundary.tsx | 4 +- .../FederatedRoomListItem.tsx | 4 +- .../MatrixFederationSearch.tsx | 4 +- .../sidebarv2/header/actions/CreateRoom.tsx | 4 +- .../client/sidebarv2/header/actions/Sort.tsx | 4 +- .../actions/hooks/useMatrixFederationItems.ts | 4 +- .../header/actions/hooks/useSortMenu.tsx | 4 +- .../sections/StatusDisabledSection.tsx | 4 +- .../contexts/TranslationContextMock.tsx | 3 +- .../DeviceManagementAccountPage.tsx | 4 +- .../DeviceManagementAccountRow.tsx | 4 +- .../DeviceManagementAccountTable.tsx | 4 +- .../AccountFeaturePreviewBadge.tsx | 4 +- .../omnichannel/PreferencesGeneral.tsx | 4 +- .../views/account/preferences/MyDataModal.tsx | 4 +- .../PreferencesHighlightsSection.tsx | 4 +- .../PreferencesMessagesSection.tsx | 4 +- .../PreferencesUserPresenceSection.tsx | 4 +- .../account/profile/ActionConfirmModal.tsx | 4 +- .../account/security/BackupCodesModal.tsx | 4 +- .../account/tokens/AccountTokensPage.tsx | 4 +- .../AccountTokensTable/AccountTokensRow.tsx | 4 +- .../admin/customEmoji/AddCustomEmoji.tsx | 4 +- .../CustomUserActiveConnections.tsx | 4 +- .../CustomUserStatusDisabledModal.tsx | 4 +- .../DeviceManagementAdminTable.tsx | 4 +- .../DeviceManagementInfoWithData.tsx | 4 +- .../EngagementDashboardCardErrorBoundary.tsx | 4 +- .../EngagementDashboardPage.tsx | 4 +- .../channels/ChannelsOverview.tsx | 9 +- .../messages/MessagesPerChannelSection.tsx | 4 +- .../messages/MessagesSentSection.tsx | 4 +- .../messages/MessagesTab.tsx | 4 +- .../users/ActiveUsersSection.tsx | 4 +- .../users/BusiestChatTimesSection.tsx | 4 +- .../users/ContentForHours.tsx | 4 +- .../users/NewUsersSection.tsx | 4 +- .../users/UsersByTimeOfTheDaySection.tsx | 4 +- .../engagementDashboard/users/UsersTab.tsx | 4 +- .../FederationDashboardPage.tsx | 4 +- .../views/admin/import/PrepareChannels.tsx | 10 +- .../views/admin/import/PrepareUsers.tsx | 10 +- .../views/admin/integrations/NewBot.tsx | 4 +- .../views/admin/integrations/NewZapier.tsx | 4 +- .../outgoing/OutgoingWebhookForm.tsx | 14 +- .../outgoing/history/HistoryContent.tsx | 4 +- .../admin/moderation/MessageContextFooter.tsx | 4 +- .../moderation/ModerationConsoleActions.tsx | 5 +- .../UserReports/ModConsoleUserActions.tsx | 4 +- .../UserReports/UserContextFooter.tsx | 4 +- .../moderation/helpers/DateRangePicker.tsx | 4 +- .../moderation/helpers/ModerationFilter.tsx | 4 +- .../helpers/ReportReasonCollapsible.tsx | 4 +- .../permissions/CustomRoleUpsellModal.tsx | 4 +- .../PermissionsTable/PermissionRow.tsx | 7 +- .../PermissionsTableFilter.tsx | 4 +- .../views/admin/permissions/RoleForm.tsx | 4 +- .../UsersInRoleTable/UsersInRoleTableRow.tsx | 4 +- .../views/admin/rooms/RoomsTableFilters.tsx | 4 +- .../ResetSettingButton/ResetSettingButton.tsx | 4 +- .../inputs/CodeMirror/CodeMirrorBox.tsx | 4 +- .../Setting/inputs/ColorSettingInput.tsx | 4 +- .../inputs/MultiSelectSettingInput.tsx | 4 +- .../Setting/inputs/SelectSettingInput.tsx | 4 +- .../inputs/TimespanSettingInput.spec.tsx | 11 +- .../Setting/inputs/TimespanSettingInput.tsx | 8 +- .../SettingsSection/SettingsSection.tsx | 4 +- .../OAuthGroupPage/CreateOAuthModal.tsx | 4 +- .../admin/settings/groups/TabbedGroupPage.tsx | 4 +- .../admin/subscription/SubscriptionPage.tsx | 2 +- .../admin/users/AdminUserFormWithData.tsx | 4 +- .../AdminUserSetRandomPasswordRadios.tsx | 4 +- .../users/SeatsCapUsage/SeatsCapUsage.tsx | 4 +- .../admin/users/UsersTable/UsersTableRow.tsx | 4 +- .../views/admin/viewLogs/AnalyticsReports.tsx | 4 +- .../views/admin/viewLogs/ViewLogsPage.tsx | 4 +- .../InstancesModal/InstancesModal.tsx | 4 +- .../MessagesRoomsCard/MessagesRoomsCard.tsx | 4 +- .../views/admin/workspace/WorkspacePage.tsx | 4 +- .../client/views/audit/AuditLogPage.tsx | 4 +- apps/meteor/client/views/audit/AuditPage.tsx | 4 +- .../audit/components/AuditFiltersDisplay.tsx | 4 +- .../views/audit/components/AuditForm.tsx | 4 +- .../views/audit/components/AuditLogEntry.tsx | 4 +- .../components/forms/DateRangePicker.tsx | 4 +- .../views/audit/components/tabs/DirectTab.tsx | 4 +- .../audit/components/tabs/OmnichannelTab.tsx | 4 +- .../views/audit/components/tabs/RoomsTab.tsx | 4 +- .../views/audit/components/tabs/UsersTab.tsx | 4 +- .../AudioMessageRecorder.tsx | 4 +- .../composer/EmojiPicker/EmojiCategoryRow.tsx | 4 +- .../EmojiPicker/EmojiPickerCategoryItem.tsx | 4 +- .../composer/EmojiPicker/SearchingResult.tsx | 4 +- .../client/views/directory/RoomTags.tsx | 4 +- .../views/e2e/EnterE2EPasswordModal.tsx | 4 +- .../client/views/e2e/SaveE2EPasswordModal.tsx | 4 +- .../views/home/cards/DesktopAppsCard.tsx | 4 +- .../views/home/cards/DocumentationCard.tsx | 4 +- .../views/home/cards/MobileAppsCard.tsx | 4 +- .../AppDetailsPage/AppDetailsPageHeader.tsx | 4 +- .../tabs/AppDetails/AppDetails.tsx | 4 +- .../AppDetailsPage/tabs/AppLogs/AppLogs.tsx | 4 +- .../tabs/AppLogs/AppLogsItem.tsx | 4 +- .../tabs/AppLogs/AppLogsItemEntry.tsx | 4 +- .../tabs/AppReleases/AppReleasesItem.tsx | 4 +- .../tabs/AppSecurity/AppSecurity.tsx | 4 +- .../tabs/AppSettings/AppSettings.tsx | 4 +- .../tabs/AppStatus/AppStatusPriceDisplay.tsx | 6 +- .../client/views/marketplace/AppMenu.tsx | 4 +- .../marketplace/AppPermissionsReviewModal.tsx | 4 +- .../views/marketplace/AppUpdateModal.tsx | 4 +- .../marketplace/AppsPage/AppsFilters.tsx | 4 +- .../AppsPage/AppsPageConnectionError.tsx | 4 +- .../AppsPage/AppsPageContentBody.tsx | 4 +- .../AppsPage/FeaturedAppsSections.tsx | 6 +- .../AppsPage/NoAppRequestsEmptyState.tsx | 4 +- .../NoInstalledAppMatchesEmptyState.tsx | 4 +- .../AppsPage/NoInstalledAppsEmptyState.tsx | 4 +- ...etplaceOrInstalledAppMatchesEmptyState.tsx | 4 +- .../AppsPage/PrivateEmptyState.tsx | 4 +- .../client/views/marketplace/BundleChips.tsx | 4 +- .../client/views/marketplace/IframeModal.tsx | 4 +- .../marketplace/UnlimitedAppsUpsellModal.tsx | 4 +- .../AppInstallModal/AppInstallModal.tsx | 4 +- .../components/AppPermissionsList.tsx | 4 +- .../CategoryFilter/CategoryDropDownAnchor.tsx | 4 +- .../components/EnabledAppsCount.tsx | 4 +- .../UninstallGrandfatheredAppModal.tsx | 4 +- .../views/marketplace/hooks/useCategories.ts | 4 +- .../views/notAuthorized/NotAuthorizedPage.tsx | 4 +- .../client/views/notFound/NotFoundPage.tsx | 4 +- .../agents/AgentsTable/AgentsTable.tsx | 4 +- .../omnichannel/analytics/AnalyticsPage.tsx | 6 +- .../omnichannel/analytics/DateRangePicker.tsx | 4 +- .../appearance/AppearanceFieldLabel.tsx | 4 +- .../omnichannel/appearance/AppearanceForm.tsx | 4 +- .../businessHours/BusinessHoursForm.tsx | 4 +- .../contactHistory/ContactHistoryItem.tsx | 4 +- .../contactHistory/ContactHistoryList.tsx | 4 +- .../MessageList/ContactHistoryMessage.tsx | 4 +- .../DepartmentAgentsTable/AgentRow.tsx | 4 +- .../DepartmentAgentsTable.tsx | 4 +- .../departments/DepartmentTags.tsx | 4 +- .../directory/calls/CallTableRow.tsx | 4 +- .../calls/contextualBar/VoipInfo.tsx | 4 +- .../contextualBar/VoipInfoCallButton.tsx | 4 +- .../chats/contextualBar/DepartmentField.tsx | 4 +- .../RoomEdit/RoomEditWithData.tsx | 4 +- .../chats/contextualBar/VisitorClientInfo.js | 4 +- .../components/CallDialpadButton.tsx | 4 +- .../directory/components/PriorityField.tsx | 4 +- .../directory/components/SlaField.tsx | 4 +- .../directory/components/SourceField.tsx | 4 +- .../contextualBar/ContactEditWithData.js | 4 +- .../omnichannel/queueList/QueueListFilter.tsx | 4 +- .../omnichannel/queueList/QueueListPage.tsx | 4 +- .../RealTimeMonitoringPage.js | 4 +- .../charts/AgentStatusChart.tsx | 9 +- .../charts/ChatDurationChart.js | 4 +- .../realTimeMonitoring/charts/ChatsChart.tsx | 9 +- .../charts/ChatsPerAgentChart.js | 4 +- .../charts/ChatsPerDepartmentChart.js | 4 +- .../charts/ResponseTimesChart.js | 4 +- .../charts/useUpdateChartData.ts | 6 +- .../counter/CounterContainer.tsx | 4 +- .../omnichannel/triggers/ConditionForm.tsx | 4 +- .../triggers/actions/ActionForm.tsx | 4 +- .../triggers/actions/ActionSender.tsx | 4 +- .../actions/ExternalServiceActionForm.tsx | 4 +- .../actions/SendMessageActionForm.tsx | 4 +- .../OutlookSettingItem.tsx | 4 +- .../views/room/E2EESetup/RoomE2EESetup.tsx | 4 +- .../QuickActions/QuickActionOptions.tsx | 4 +- .../Omnichannel/QuickActions/QuickActions.tsx | 4 +- .../client/views/room/Header/RoomHeader.tsx | 4 +- .../RoomToolbox/RoomToolboxE2EESetup.tsx | 4 +- .../QuickActions/QuickActionOptions.tsx | 4 +- .../Omnichannel/QuickActions/QuickActions.tsx | 4 +- .../client/views/room/HeaderV2/RoomHeader.tsx | 4 +- .../client/views/room/HeaderV2/RoomLeader.tsx | 4 +- .../RoomToolbox/RoomToolboxE2EESetup.tsx | 4 +- .../MessageList/MessageListErrorBoundary.tsx | 4 +- .../room/MessageList/MessageListItem.tsx | 4 +- .../views/room/body/DropTargetOverlay.tsx | 4 +- .../body/ErroredUploadProgressIndicator.tsx | 4 +- .../client/views/room/body/LeaderBar.tsx | 4 +- .../room/body/RetentionPolicyWarning.tsx | 4 +- .../room/body/RoomForeword/RoomForeword.tsx | 4 +- .../room/body/UnreadMessagesIndicator.tsx | 4 +- .../room/body/UploadProgressIndicator.tsx | 4 +- .../views/room/composer/ComposerArchived.tsx | 4 +- .../views/room/composer/ComposerBlocked.tsx | 4 +- .../views/room/composer/ComposerBoxPopup.tsx | 4 +- .../composer/ComposerBoxPopupSlashCommand.tsx | 4 +- .../room/composer/ComposerBoxPopupUser.tsx | 4 +- .../ComposerFederationDisabled.tsx | 4 +- .../ComposerFederationJoinRoomDisabled.tsx | 4 +- .../ComposerOmnichannelOnHold.tsx | 4 +- .../ComposerUserActionIndicator.tsx | 4 +- .../views/room/composer/ComposerVoIP.tsx | 4 +- .../FormattingToolbarDropdown.tsx | 4 +- .../MessageBoxFormattingToolbar.tsx | 4 +- .../messageBox/hooks/useMediaActionTitle.ts | 4 +- .../AutoTranslate/AutoTranslate.tsx | 4 +- .../Discussions/DiscussionsListRow.tsx | 4 +- .../components/DiscussionsListItem.tsx | 4 +- .../ExportMessages/ExportMessages.tsx | 4 +- .../ExportMessages/FileExport.tsx | 4 +- .../ExportMessages/MailExportForm.tsx | 4 +- .../ChannelToTeamConfirmation.tsx | 4 +- .../ChannelToTeamSelection.tsx | 4 +- .../contextualBar/Info/RoomInfo/RoomInfo.tsx | 4 +- .../Info/hooks/useRoomActions.ts | 4 +- .../KeyboardShortcuts/KeyboardShortcuts.tsx | 4 +- .../MessageSearchTab/MessageSearchTab.tsx | 4 +- .../components/MessageSearchForm.tsx | 4 +- .../NotificationPreferences.tsx | 4 +- .../NotificationPreferencesForm.tsx | 4 +- .../views/room/contextualBar/OTR/OTR.tsx | 4 +- .../OTR/components/OTREstablished.tsx | 4 +- .../OTR/components/OTRStates.tsx | 4 +- .../PruneMessages/PruneMessages.tsx | 4 +- .../contextualBar/RoomFiles/RoomFiles.tsx | 4 +- .../InviteUsers/EditInviteLink.tsx | 4 +- .../RoomMembers/InviteUsers/InviteLink.tsx | 4 +- .../RoomMembers/InviteUsers/InviteUsers.tsx | 4 +- .../RoomMembers/RoomMembersActions.tsx | 4 +- .../Threads/components/ThreadListMessage.tsx | 4 +- .../Threads/components/ThreadMessageItem.tsx | 4 +- .../UserInfo/UserInfoActions.tsx | 4 +- .../VideoConference/VideoConfConfigModal.tsx | 4 +- .../VideoConfList/VideoConfList.tsx | 4 +- .../VideoConfPopup/IncomingPopup.tsx | 4 +- .../VideoConfPopup/OutgoingPopup.tsx | 4 +- .../VideoConfPopup/StartCallPopup.tsx | 4 +- .../modals/FileUploadModal/MediaPreview.tsx | 4 +- .../ReactionListModal/ReactionListModal.tsx | 4 +- .../FilePickerBreadcrumbs.tsx | 4 +- .../WebdavFilePickerTable.tsx | 4 +- .../ChannelDesertionTable.tsx | 4 +- .../ModalSteps/FirstStep.tsx | 4 +- .../ModalSteps/SecondStep.tsx | 4 +- .../channels/TeamsChannelItemMenu.tsx | 4 +- .../contextualBar/channels/TeamsChannels.tsx | 4 +- .../ChannelDeletionTable.tsx | 4 +- .../info/DeleteTeam/DeleteTeamChannels.tsx | 4 +- .../DeleteTeam/DeleteTeamConfirmation.tsx | 4 +- .../LeaveTeamModal/LeaveTeamModalChannels.tsx | 4 +- .../LeaveTeamModalConfirmation.tsx | 4 +- .../teams/contextualBar/info/TeamsInfo.tsx | 4 +- .../RemoveUsersModal/RemoveUsersFirstStep.js | 4 +- .../RemoveUsersModal/RemoveUsersSecondStep.js | 4 +- .../client/voip/modal/DialPad/DialInput.tsx | 4 +- .../voip/modal/DialPad/hooks/useDialPad.tsx | 4 +- apps/meteor/package.json | 4 +- .../EmailInbox/EmailInbox_Incoming.ts | 2 +- .../EmailInbox/EmailInbox_Outgoing.ts | 2 +- apps/meteor/server/lib/i18n.ts | 11 - apps/meteor/server/lib/resetUserE2EKey.ts | 2 +- .../modules/core-apps/mention.module.ts | 44 +- .../services/omnichannel-analytics/service.ts | 2 +- package.json | 3 +- packages/fuselage-ui-kit/package.json | 4 +- packages/gazzodown/package.json | 47 +-- packages/gazzodown/src/elements/LinkSpan.tsx | 4 +- packages/gazzodown/src/elements/PlainSpan.tsx | 4 +- .../src/mentions/ChannelMentionElement.tsx | 4 +- .../src/mentions/UserMentionElement.tsx | 4 +- packages/i18n/jest.config.ts | 2 +- packages/i18n/package.json | 31 +- packages/i18n/src/index.mjs | 14 +- packages/jest-presets/package.json | 2 +- packages/livechat/package.json | 4 +- packages/mock-providers/package.json | 4 +- packages/ui-client/package.json | 34 +- .../src/components/CustomFieldsForm.tsx | 4 +- .../GenericMenu/GenericMenu.spec.tsx | 7 +- .../components/GenericMenu/GenericMenu.tsx | 10 +- .../MultiSelectCustomAnchor.tsx | 4 +- .../MultiSelectCustomList.tsx | 4 +- .../MultiSelectCustom/useFilteredOptions.tsx | 4 +- .../src/components/UserStatus/UserStatus.tsx | 4 +- packages/web-ui-registration/package.json | 8 +- .../src/SecretRegisterInvalidForm.tsx | 4 +- yarn.lock | 380 ++++++------------ 425 files changed, 1208 insertions(+), 1238 deletions(-) create mode 100644 .yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch diff --git a/.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch b/.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch new file mode 100644 index 000000000000..cf5a292c0253 --- /dev/null +++ b/.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch @@ -0,0 +1,99 @@ +diff --git a/dist/amd/react-i18next.js b/dist/amd/react-i18next.js +index 115ef3cc362e5ce38e875f9b35dfd1fe687cfd6c..2ba1b4d54295eeff49c8c650f214d7875d9219a5 100644 +--- a/dist/amd/react-i18next.js ++++ b/dist/amd/react-i18next.js +@@ -495,7 +495,7 @@ define(['exports', 'react'], (function (exports, react) { 'use strict'; + } + addUsedNamespaces(namespaces) { + namespaces.forEach(ns => { +- this.usedNamespaces[ns] ??= true; ++ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; + }); + } + getUsedNamespaces() { +diff --git a/dist/amd/react-i18next.min.js b/dist/amd/react-i18next.min.js +index c54c9114869feb14df7855be24237f85d64fe7e4..0cc2b09d6db9bfc6f94dffff4b9e0f3fd9108510 100644 +--- a/dist/amd/react-i18next.min.js ++++ b/dist/amd/react-i18next.min.js +@@ -1 +1 @@ +-define(["exports","react"],(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),a=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function i(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var i=e.indexOf("--\x3e");return{type:"comment",comment:-1!==i?e.slice(4,i):""}}for(var r=new RegExp(a),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],a=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=i(r);return c<0?(s.push(g),s):((p=a[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=i(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=a[c-1])&&p.children.push(t),a[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:a[c]),!u&&"<"!==m&&m)){p=-1===c?s:a[c].children;var y=e.indexOf("<",h),v=e.slice(h,-1===y?void 0:y);o.test(v)&&(v=" "),(y>-1&&c+p.length>=0||" "!==v)&&p.push({type:"text",content:v})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,v=e=>"object"==typeof e&&null!==e,x=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,b={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>b[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(x,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],C=(e,t)=>{if(!e)return"";let s="";const a=R(e),i=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return a.forEach(((e,a)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=i.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${a}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=C(c,t);s+=`<${a}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(v(e)){const{format:n,...t}=e,a=Object.keys(t);if(1===a.length){const e=n?`${a[0]}, ${n}`:a[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},T=(e,t,s,a,i,r)=>{if(""===t)return[];const o=a.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):v(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...i},h=(e,t,s)=>{const a=j(e),i=g(a,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(a)&&0===i.length||e.props?.i18nIsDynamicList?a:i},m=(e,t,s,a,i)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:a},i?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:a,ref:e.ref},i?null:t)})))},g=(t,i,c)=>{const u=R(t);return R(i).reduce(((t,i,p)=>{const d=i.children?.[0]?.content&&s.services.interpolator.interpolate(i.children[0].content,f,s.language);if("tag"===i.type){let r=u[parseInt(i.name,10)];1!==c.length||r||(r=c[0][i.name]),r||(r={});const x=0!==Object.keys(i.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:i.attrs},r):r,b=n.isValidElement(x),E=b&&S(i,!0)&&!i.voidElement,O=l&&v(x)&&x.dummy&&!b,N=v(e)&&Object.hasOwnProperty.call(e,i.name);if(y(x)){const e=s.services.interpolator.interpolate(x,f,s.language);t.push(e)}else if(S(x)||E){const e=h(x,i,c);m(x,e,t,p)}else if(O){const e=g(u,i.children,c);m(x,e,t,p)}else if(Number.isNaN(parseFloat(i.name)))if(N){const e=h(x,i,c);m(x,e,t,p,i.voidElement)}else if(a.transSupportBasicHtmlNodes&&o.indexOf(i.name)>-1)if(i.voidElement)t.push(n.createElement(i.name,{key:`${i.name}-${p}`}));else{const e=g(u,i.children,c);t.push(n.createElement(i.name,{key:`${i.name}-${p}`},e))}else if(i.voidElement)t.push(`<${i.name} />`);else{const e=g(u,i.children,c);t.push(`<${i.name}>${e}`)}else if(v(x)&&!b){const e=i.children[0]?d:null;e&&t.push(e)}else m(x,d,t,p,1!==i.children.length||!d)}else if("text"===i.type){const e=a.transWrapTextNodes,o=r?a.unescape(s.services.interpolator.interpolate(i.content,f,s.language)):s.services.interpolator.interpolate(i.content,f,s.language);e?t.push(n.createElement(e,{key:`${i.name}-${p}`},o)):t.push(o)}return t}),[])},x=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(x[0])};function P(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const v=f||k();if(!v)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const x=h||v.t.bind(v)||(e=>e),b={...$(),...v.options?.react};let E=p||x.ns||v.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=C(t,b),N=c||O||b.transEmptyNodeValue||i,{hashTransKey:w}=b,I=i||(w?w(O||N):O||N);v.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...v.options.interpolation.defaultVariables}:{...v.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?x(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=T(u||t,R,v,b,j,m),L=a??b.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]??=!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:a,defaultNS:i}=n.useContext(A)||{},r=s||a||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:v(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||i||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,a)=>n.useCallback(U(e,t,s,a),[e,t,s,a]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,x=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[b,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(x)})):h(r,u,(()=>{w.current&&E(x)}))),p&&N&&N!==O&&w.current&&E(x);const s=()=>{w.current&&E(x)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[b,r,p];if(I.t=b,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:a}=s,{i18n:i}=n.useContext(A)||{},r=a||i||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:a}=e;const i=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:i},a)},e.Trans=function(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},v=d||g||k(),x=f||v?.t.bind(v);return P({children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||x?.ns||y||v?.options?.defaultNS,i18n:v,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[a,i,r]=B(n,s);return t(a,{i18n:i,lng:i.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:a,...i}=t;return D(s,a),n.createElement(e,{...i})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function a(a){let{forwardedRef:i,...r}=a;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&i?u.ref=i:!t.withRef&&i&&(u.forwardedRef=i),n.createElement(s,u)}a.displayName=`withI18nextTranslation(${g(s)})`,a.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(a,Object.assign({},e,{forwardedRef:t})))):a}}})); ++define(["exports","react"],(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),a=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function i(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var i=e.indexOf("--\x3e");return{type:"comment",comment:-1!==i?e.slice(4,i):""}}for(var r=new RegExp(a),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],a=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=i(r);return c<0?(s.push(g),s):((p=a[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=i(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=a[c-1])&&p.children.push(t),a[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:a[c]),!u&&"<"!==m&&m)){p=-1===c?s:a[c].children;var y=e.indexOf("<",h),v=e.slice(h,-1===y?void 0:y);o.test(v)&&(v=" "),(y>-1&&c+p.length>=0||" "!==v)&&p.push({type:"text",content:v})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,v=e=>"object"==typeof e&&null!==e,x=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,b={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>b[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(x,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],C=(e,t)=>{if(!e)return"";let s="";const a=R(e),i=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return a.forEach(((e,a)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=i.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${a}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=C(c,t);s+=`<${a}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(v(e)){const{format:n,...t}=e,a=Object.keys(t);if(1===a.length){const e=n?`${a[0]}, ${n}`:a[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},T=(e,t,s,a,i,r)=>{if(""===t)return[];const o=a.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):v(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...i},h=(e,t,s)=>{const a=j(e),i=g(a,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(a)&&0===i.length||e.props?.i18nIsDynamicList?a:i},m=(e,t,s,a,i)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:a},i?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:a,ref:e.ref},i?null:t)})))},g=(t,i,c)=>{const u=R(t);return R(i).reduce(((t,i,p)=>{const d=i.children?.[0]?.content&&s.services.interpolator.interpolate(i.children[0].content,f,s.language);if("tag"===i.type){let r=u[parseInt(i.name,10)];1!==c.length||r||(r=c[0][i.name]),r||(r={});const x=0!==Object.keys(i.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:i.attrs},r):r,b=n.isValidElement(x),E=b&&S(i,!0)&&!i.voidElement,O=l&&v(x)&&x.dummy&&!b,N=v(e)&&Object.hasOwnProperty.call(e,i.name);if(y(x)){const e=s.services.interpolator.interpolate(x,f,s.language);t.push(e)}else if(S(x)||E){const e=h(x,i,c);m(x,e,t,p)}else if(O){const e=g(u,i.children,c);m(x,e,t,p)}else if(Number.isNaN(parseFloat(i.name)))if(N){const e=h(x,i,c);m(x,e,t,p,i.voidElement)}else if(a.transSupportBasicHtmlNodes&&o.indexOf(i.name)>-1)if(i.voidElement)t.push(n.createElement(i.name,{key:`${i.name}-${p}`}));else{const e=g(u,i.children,c);t.push(n.createElement(i.name,{key:`${i.name}-${p}`},e))}else if(i.voidElement)t.push(`<${i.name} />`);else{const e=g(u,i.children,c);t.push(`<${i.name}>${e}`)}else if(v(x)&&!b){const e=i.children[0]?d:null;e&&t.push(e)}else m(x,d,t,p,1!==i.children.length||!d)}else if("text"===i.type){const e=a.transWrapTextNodes,o=r?a.unescape(s.services.interpolator.interpolate(i.content,f,s.language)):s.services.interpolator.interpolate(i.content,f,s.language);e?t.push(n.createElement(e,{key:`${i.name}-${p}`},o)):t.push(o)}return t}),[])},x=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(x[0])};function P(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const v=f||k();if(!v)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const x=h||v.t.bind(v)||(e=>e),b={...$(),...v.options?.react};let E=p||x.ns||v.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=C(t,b),N=c||O||b.transEmptyNodeValue||i,{hashTransKey:w}=b,I=i||(w?w(O||N):O||N);v.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...v.options.interpolation.defaultVariables}:{...v.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?x(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=T(u||t,R,v,b,j,m),L=a??b.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]=this.usedNamespaces[e]??!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:a,defaultNS:i}=n.useContext(A)||{},r=s||a||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:v(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||i||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,a)=>n.useCallback(U(e,t,s,a),[e,t,s,a]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,x=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[b,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(x)})):h(r,u,(()=>{w.current&&E(x)}))),p&&N&&N!==O&&w.current&&E(x);const s=()=>{w.current&&E(x)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[b,r,p];if(I.t=b,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:a}=s,{i18n:i}=n.useContext(A)||{},r=a||i||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:a}=e;const i=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:i},a)},e.Trans=function(e){let{children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},v=d||g||k(),x=f||v?.t.bind(v);return P({children:t,count:s,parent:a,i18nKey:i,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||x?.ns||y||v?.options?.defaultNS,i18n:v,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[a,i,r]=B(n,s);return t(a,{i18n:i,lng:i.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:a,...i}=t;return D(s,a),n.createElement(e,{...i})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function a(a){let{forwardedRef:i,...r}=a;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&i?u.ref=i:!t.withRef&&i&&(u.forwardedRef=i),n.createElement(s,u)}a.displayName=`withI18nextTranslation(${g(s)})`,a.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(a,Object.assign({},e,{forwardedRef:t})))):a}}})); +diff --git a/dist/commonjs/context.js b/dist/commonjs/context.js +index 5c4506e4d3424e4ffd167fd5bb696dabd67b662b..dcc11b798da72771953acce3c1368f006c3c1478 100644 +--- a/dist/commonjs/context.js ++++ b/dist/commonjs/context.js +@@ -46,7 +46,7 @@ class ReportNamespaces { + } + addUsedNamespaces(namespaces) { + namespaces.forEach(ns => { +- this.usedNamespaces[ns] ??= true; ++ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; + }); + } + getUsedNamespaces() { +diff --git a/dist/es/context.js b/dist/es/context.js +index 89afe45d6cf480079598dd183521b32f28f77f06..a953078f44008ee751f51257536a31ceba2fd672 100644 +--- a/dist/es/context.js ++++ b/dist/es/context.js +@@ -10,7 +10,7 @@ export class ReportNamespaces { + } + addUsedNamespaces(namespaces) { + namespaces.forEach(ns => { +- this.usedNamespaces[ns] ??= true; ++ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; + }); + } + getUsedNamespaces() { +diff --git a/dist/umd/react-i18next.js b/dist/umd/react-i18next.js +index 3723bd7c0f5762bdb09e3226ac86ff255cbf9859..9178ae9f7cdb776f51a64ff6c79eed1d18fbd836 100644 +--- a/dist/umd/react-i18next.js ++++ b/dist/umd/react-i18next.js +@@ -499,7 +499,7 @@ + } + addUsedNamespaces(namespaces) { + namespaces.forEach(ns => { +- this.usedNamespaces[ns] ??= true; ++ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; + }); + } + getUsedNamespaces() { +diff --git a/dist/umd/react-i18next.min.js b/dist/umd/react-i18next.min.js +index 2eef624040aab6b4b9ba4699bf7e4777842bf0a2..69e17753d545df9dc26aa3411b477a4dff5e8361 100644 +--- a/dist/umd/react-i18next.min.js ++++ b/dist/umd/react-i18next.min.js +@@ -1 +1 @@ +-!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]??=!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); ++!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]=this.usedNamespaces[e]??!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); +diff --git a/react-i18next.js b/react-i18next.js +index 3723bd7c0f5762bdb09e3226ac86ff255cbf9859..9178ae9f7cdb776f51a64ff6c79eed1d18fbd836 100644 +--- a/react-i18next.js ++++ b/react-i18next.js +@@ -499,7 +499,7 @@ + } + addUsedNamespaces(namespaces) { + namespaces.forEach(ns => { +- this.usedNamespaces[ns] ??= true; ++ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; + }); + } + getUsedNamespaces() { +diff --git a/react-i18next.min.js b/react-i18next.min.js +index 2eef624040aab6b4b9ba4699bf7e4777842bf0a2..69e17753d545df9dc26aa3411b477a4dff5e8361 100644 +--- a/react-i18next.min.js ++++ b/react-i18next.min.js +@@ -1 +1 @@ +-!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]??=!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); ++!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("react")):"function"==typeof define&&define.amd?define(["exports","react"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).ReactI18next={},e.React)}(this,(function(e,n){"use strict";function t(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var s=t({area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0}),i=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;function a(e){var n={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},t=e.match(/<\/?([^\s]+?)[/\s>]/);if(t&&(n.name=t[1],(s[t[1]]||"/"===e.charAt(e.length-2))&&(n.voidElement=!0),n.name.startsWith("!--"))){var a=e.indexOf("--\x3e");return{type:"comment",comment:-1!==a?e.slice(4,a):""}}for(var r=new RegExp(i),o=null;null!==(o=r.exec(e));)if(o[0].trim())if(o[1]){var l=o[1].trim(),c=[l,""];l.indexOf("=")>-1&&(c=l.split("=")),n.attrs[c[0]]=c[1],r.lastIndex--}else o[2]&&(n.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return n}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,o=/^\s*$/,l=Object.create(null);var c=function(e,n){n||(n={}),n.components||(n.components=l);var t,s=[],i=[],c=-1,u=!1;if(0!==e.indexOf("<")){var p=e.indexOf("<");s.push({type:"text",content:-1===p?e:e.substring(0,p)})}return e.replace(r,(function(r,l){if(u){if(r!=="")return;u=!1}var p,d="/"!==r.charAt(1),f=r.startsWith("\x3c!--"),h=l+r.length,m=e.charAt(h);if(f){var g=a(r);return c<0?(s.push(g),s):((p=i[c]).children.push(g),s)}if(d&&(c++,"tag"===(t=a(r)).type&&n.components[t.name]&&(t.type="component",u=!0),t.voidElement||u||!m||"<"===m||t.children.push({type:"text",content:e.slice(h,e.indexOf("<",h))}),0===c&&s.push(t),(p=i[c-1])&&p.children.push(t),i[c]=t),(!d||t.voidElement)&&(c>-1&&(t.voidElement||t.name===r.slice(2,-1))&&(c--,t=-1===c?s:i[c]),!u&&"<"!==m&&m)){p=-1===c?s:i[c].children;var y=e.indexOf("<",h),x=e.slice(h,-1===y?void 0:y);o.test(x)&&(x=" "),(y>-1&&c+p.length>=0||" "!==x)&&p.push({type:"text",content:x})}})),s};const u=function(){if(console?.warn){for(var e=arguments.length,n=new Array(e),t=0;t()=>{if(e.isInitialized)n();else{const t=()=>{setTimeout((()=>{e.off("initialized",t)}),0),n()};e.on("initialized",t)}},h=(e,n,t)=>{e.loadNamespaces(n,f(e,t))},m=(e,n,t,s)=>{y(t)&&(t=[t]),t.forEach((n=>{e.options.ns.indexOf(n)<0&&e.options.ns.push(n)})),e.loadLanguages(n,f(e,s))},g=e=>e.displayName||e.name||(y(e)&&e.length>0?e:"Unknown"),y=e=>"string"==typeof e,x=e=>"object"==typeof e&&null!==e,b=/&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34|nbsp|#160|copy|#169|reg|#174|hellip|#8230|#x2F|#47);/g,v={"&":"&","&":"&","<":"<","<":"<",">":">",">":">","'":"'","'":"'",""":'"',""":'"'," ":" "," ":" ","©":"©","©":"©","®":"®","®":"®","…":"…","…":"…","/":"/","/":"/"},E=e=>v[e];let O={bindI18n:"languageChanged",bindI18nStore:"",transEmptyNodeValue:"",transSupportBasicHtmlNodes:!0,transWrapTextNodes:"",transKeepBasicHtmlNodesFor:["br","strong","i","p"],useSuspense:!0,unescape:e=>e.replace(b,E)};const N=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};O={...O,...e}},$=()=>O;let w;const I=e=>{w=e},k=()=>w,S=(e,n)=>{if(!e)return!1;const t=e.props?.children??e.children;return n?t.length>0:!!t},j=e=>{if(!e)return[];const n=e.props?.children??e.children;return e.props?.i18nIsDynamicList?R(n):n},R=e=>Array.isArray(e)?e:[e],T=(e,t)=>{if(!e)return"";let s="";const i=R(e),a=t?.transSupportBasicHtmlNodes?t.transKeepBasicHtmlNodesFor??[]:[];return i.forEach(((e,i)=>{if(y(e))s+=`${e}`;else if(n.isValidElement(e)){const{props:n,type:r}=e,o=Object.keys(n).length,l=a.indexOf(r)>-1,c=n.children;if(c||!l||o)if(!c&&(!l||o)||n.i18nIsDynamicList)s+=`<${i}>`;else if(l&&1===o&&y(c))s+=`<${r}>${c}`;else{const e=T(c,t);s+=`<${i}>${e}`}else s+=`<${r}/>`}else if(null===e)u("Trans: the passed in value is invalid - seems you passed in a null child.");else if(x(e)){const{format:n,...t}=e,i=Object.keys(t);if(1===i.length){const e=n?`${i[0]}, ${n}`:i[0];s+=`{{${e}}}`}else u("react-i18next: the passed in object contained more than one variable - the object should look like {{ value, format }} where format is optional.",e)}else u("Trans: the passed in value is invalid - seems you passed in a variable like {number} - please pass in variables for interpolation as full objects like {{number}}.",e)})),s},C=(e,t,s,i,a,r)=>{if(""===t)return[];const o=i.transKeepBasicHtmlNodesFor||[],l=t&&new RegExp(o.map((e=>`<${e}`)).join("|")).test(t);if(!e&&!l&&!r)return[t];const u={},p=e=>{R(e).forEach((e=>{y(e)||(S(e)?p(j(e)):x(e)&&!n.isValidElement(e)&&Object.assign(u,e))}))};p(e);const d=c(`<0>${t}`),f={...u,...a},h=(e,t,s)=>{const i=j(e),a=g(i,t.children,s);return(e=>Array.isArray(e)&&e.every(n.isValidElement))(i)&&0===a.length||e.props?.i18nIsDynamicList?i:a},m=(e,t,s,i,a)=>{e.dummy?(e.children=t,s.push(n.cloneElement(e,{key:i},a?void 0:t))):s.push(...n.Children.map([e],(e=>{const s={...e.props};return delete s.i18nIsDynamicList,n.createElement(e.type,{...s,key:i,ref:e.ref},a?null:t)})))},g=(t,a,c)=>{const u=R(t);return R(a).reduce(((t,a,p)=>{const d=a.children?.[0]?.content&&s.services.interpolator.interpolate(a.children[0].content,f,s.language);if("tag"===a.type){let r=u[parseInt(a.name,10)];1!==c.length||r||(r=c[0][a.name]),r||(r={});const b=0!==Object.keys(a.attrs).length?((e,n)=>{const t={...n};return t.props=Object.assign(e.props,n.props),t})({props:a.attrs},r):r,v=n.isValidElement(b),E=v&&S(a,!0)&&!a.voidElement,O=l&&x(b)&&b.dummy&&!v,N=x(e)&&Object.hasOwnProperty.call(e,a.name);if(y(b)){const e=s.services.interpolator.interpolate(b,f,s.language);t.push(e)}else if(S(b)||E){const e=h(b,a,c);m(b,e,t,p)}else if(O){const e=g(u,a.children,c);m(b,e,t,p)}else if(Number.isNaN(parseFloat(a.name)))if(N){const e=h(b,a,c);m(b,e,t,p,a.voidElement)}else if(i.transSupportBasicHtmlNodes&&o.indexOf(a.name)>-1)if(a.voidElement)t.push(n.createElement(a.name,{key:`${a.name}-${p}`}));else{const e=g(u,a.children,c);t.push(n.createElement(a.name,{key:`${a.name}-${p}`},e))}else if(a.voidElement)t.push(`<${a.name} />`);else{const e=g(u,a.children,c);t.push(`<${a.name}>${e}`)}else if(x(b)&&!v){const e=a.children[0]?d:null;e&&t.push(e)}else m(b,d,t,p,1!==a.children.length||!d)}else if("text"===a.type){const e=i.transWrapTextNodes,o=r?i.unescape(s.services.interpolator.interpolate(a.content,f,s.language)):s.services.interpolator.interpolate(a.content,f,s.language);e?t.push(n.createElement(e,{key:`${a.name}-${p}`},o)):t.push(o)}return t}),[])},b=g([{dummy:!0,children:e||[]}],d,R(e||[]));return j(b[0])};function P(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:f,t:h,shouldUnescape:m,...g}=e;const x=f||k();if(!x)return d("You will need to pass in an i18next instance by using i18nextReactModule"),t;const b=h||x.t.bind(x)||(e=>e),v={...$(),...x.options?.react};let E=p||b.ns||x.options?.defaultNS;E=y(E)?[E]:E||["translation"];const O=T(t,v),N=c||O||v.transEmptyNodeValue||a,{hashTransKey:w}=v,I=a||(w?w(O||N):O||N);x.options?.interpolation?.defaultVariables&&(l=l&&Object.keys(l).length>0?{...l,...x.options.interpolation.defaultVariables}:{...x.options.interpolation.defaultVariables});const S=l||void 0!==s||!t?o.interpolation:{interpolation:{...o.interpolation,prefix:"#$?",suffix:"?$#"}},j={...o,context:r||o.context,count:s,...l,...S,defaultValue:N,ns:E},R=I?b(I,j):N;u&&Object.keys(u).forEach((e=>{const t=u[e];"function"==typeof t.type||!t.props||!t.props.children||R.indexOf(`${e}/>`)<0&&R.indexOf(`${e} />`)<0||(u[e]=n.createElement((function(){return n.createElement(n.Fragment,null,t)})))}));const P=C(u||t,R,x,v,j,m),L=i??v.defaultTransParent;return L?n.createElement(L,g,P):P}const L={type:"3rdParty",init(e){N(e.options.react),I(e)}},A=n.createContext();class V{constructor(){this.usedNamespaces={}}addUsedNamespaces(e){e.forEach((e=>{this.usedNamespaces[e]=this.usedNamespaces[e]??!0}))}getUsedNamespaces(){return Object.keys(this.usedNamespaces)}}const z=e=>async n=>({...await(e.getInitialProps?.(n))??{},...F()}),F=()=>{const e=k(),n=e.reportNamespaces?.getUsedNamespaces()??[],t={},s={};return e.languages.forEach((t=>{s[t]={},n.forEach((n=>{s[t][n]=e.getResourceBundle(t,n)||{}}))})),t.initialI18nStore=s,t.initialLanguage=e.language,t};const U=(e,n,t,s)=>e.getFixedT(n,t,s),B=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const{i18n:s}=t,{i18n:i,defaultNS:a}=n.useContext(A)||{},r=s||i||k();if(r&&!r.reportNamespaces&&(r.reportNamespaces=new V),!r){d("You will need to pass in an i18next instance by using initReactI18next");const e=(e,n)=>y(n)?n:x(n)&&y(n.defaultValue)?n.defaultValue:Array.isArray(e)?e[e.length-1]:e,n=[e,{},!1];return n.t=e,n.i18n={},n.ready=!1,n}r.options.react?.wait&&d("It seems you are still using the old wait option, you may migrate to the new useSuspense behaviour.");const o={...$(),...r.options.react,...t},{useSuspense:l,keyPrefix:c}=o;let u=e||a||r.options?.defaultNS;u=y(u)?[u]:u||["translation"],r.reportNamespaces.addUsedNamespaces?.(u);const p=(r.isInitialized||r.initializedStoreOnce)&&u.every((e=>function(e,n){let t=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.languages&&n.languages.length?n.hasLoadedNamespace(e,{lng:t.lng,precheck:(n,s)=>{if(t.bindI18n?.indexOf("languageChanging")>-1&&n.services.backendConnector.backend&&n.isLanguageChangingTo&&!s(n.isLanguageChangingTo,e))return!1}}):(d("i18n.languages were undefined or empty",n.languages),!0)}(e,r,o))),f=((e,t,s,i)=>n.useCallback(U(e,t,s,i),[e,t,s,i]))(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),g=()=>f,b=()=>U(r,t.lng||null,"fallback"===o.nsMode?u:u[0],c),[v,E]=n.useState(g);let O=u.join();t.lng&&(O=`${t.lng}${O}`);const N=((e,t)=>{const s=n.useRef();return n.useEffect((()=>{s.current=e}),[e,t]),s.current})(O),w=n.useRef(!0);n.useEffect((()=>{const{bindI18n:e,bindI18nStore:n}=o;w.current=!0,p||l||(t.lng?m(r,t.lng,u,(()=>{w.current&&E(b)})):h(r,u,(()=>{w.current&&E(b)}))),p&&N&&N!==O&&w.current&&E(b);const s=()=>{w.current&&E(b)};return e&&r?.on(e,s),n&&r?.store.on(n,s),()=>{w.current=!1,r&&e?.split(" ").forEach((e=>r.off(e,s))),n&&r&&n.split(" ").forEach((e=>r.store.off(e,s)))}}),[r,O]),n.useEffect((()=>{w.current&&p&&E(g)}),[r,c,p]);const I=[v,r,p];if(I.t=v,I.i18n=r,I.ready=p,p)return I;if(!p&&!l)return I;throw new Promise((e=>{t.lng?m(r,t.lng,u,(()=>e())):h(r,u,(()=>e()))}))};const D=function(e,t){let s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const{i18n:i}=s,{i18n:a}=n.useContext(A)||{},r=i||a||k();r.options?.isClone||(e&&!r.initializedStoreOnce&&(r.services.resourceStore.data=e,r.options.ns=Object.values(e).reduce(((e,n)=>(Object.keys(n).forEach((n=>{e.indexOf(n)<0&&e.push(n)})),e)),r.options.ns),r.initializedStoreOnce=!0,r.isInitialized=!0),t&&!r.initializedLanguageOnce&&(r.changeLanguage(t),r.initializedLanguageOnce=!0))};e.I18nContext=A,e.I18nextProvider=function(e){let{i18n:t,defaultNS:s,children:i}=e;const a=n.useMemo((()=>({i18n:t,defaultNS:s})),[t,s]);return n.createElement(A.Provider,{value:a},i)},e.Trans=function(e){let{children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o={},values:l,defaults:c,components:u,ns:p,i18n:d,t:f,shouldUnescape:h,...m}=e;const{i18n:g,defaultNS:y}=n.useContext(A)||{},x=d||g||k(),b=f||x?.t.bind(x);return P({children:t,count:s,parent:i,i18nKey:a,context:r,tOptions:o,values:l,defaults:c,components:u,ns:p||b?.ns||y||x?.options?.defaultNS,i18n:x,t:f,shouldUnescape:h,...m})},e.TransWithoutContext=P,e.Translation=e=>{let{ns:n,children:t,...s}=e;const[i,a,r]=B(n,s);return t(i,{i18n:a,lng:a.language},r)},e.composeInitialProps=z,e.date=()=>"",e.getDefaults=$,e.getI18n=k,e.getInitialProps=F,e.initReactI18next=L,e.number=()=>"",e.plural=()=>"",e.select=()=>"",e.selectOrdinal=()=>"",e.setDefaults=N,e.setI18n=I,e.time=()=>"",e.useSSR=D,e.useTranslation=B,e.withSSR=()=>function(e){function t(t){let{initialI18nStore:s,initialLanguage:i,...a}=t;return D(s,i),n.createElement(e,{...a})}return t.getInitialProps=z(e),t.displayName=`withI18nextSSR(${g(e)})`,t.WrappedComponent=e,t},e.withTranslation=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return function(s){function i(i){let{forwardedRef:a,...r}=i;const[o,l,c]=B(e,{...r,keyPrefix:t.keyPrefix}),u={...r,t:o,i18n:l,tReady:c};return t.withRef&&a?u.ref=a:!t.withRef&&a&&(u.forwardedRef=a),n.createElement(s,u)}i.displayName=`withI18nextTranslation(${g(s)})`,i.WrappedComponent=s;return t.withRef?n.forwardRef(((e,t)=>n.createElement(i,Object.assign({},e,{forwardedRef:t})))):i}}})); +diff --git a/src/context.js b/src/context.js +index 167af9c50f47e34f7473df03bb2bb1e369725934..9b9a5570b0b765c9d7809f912dba2db759ebb68d 100644 +--- a/src/context.js ++++ b/src/context.js +@@ -14,7 +14,7 @@ export class ReportNamespaces { + + addUsedNamespaces(namespaces) { + namespaces.forEach((ns) => { +- this.usedNamespaces[ns] ??= true; ++ this.usedNamespaces[ns] = this.usedNamespaces[ns] ?? true; + }); + } + diff --git a/apps/meteor/app/2fa/server/code/EmailCheck.ts b/apps/meteor/app/2fa/server/code/EmailCheck.ts index d947c1b30c2e..5baf218a62bb 100644 --- a/apps/meteor/app/2fa/server/code/EmailCheck.ts +++ b/apps/meteor/app/2fa/server/code/EmailCheck.ts @@ -38,7 +38,7 @@ export class EmailCheck implements ICodeCheck { private async send2FAEmail(address: string, random: string, user: IUser): Promise { const language = user.language || settings.get('Language') || 'en'; - const t = (s: string): string => i18n.t(s, { lng: language }); + const t = i18n.getFixedT(language); await Mailer.send({ to: address, diff --git a/apps/meteor/app/2fa/server/functions/resetTOTP.ts b/apps/meteor/app/2fa/server/functions/resetTOTP.ts index 3be8ec7c8060..84426cd4f88d 100644 --- a/apps/meteor/app/2fa/server/functions/resetTOTP.ts +++ b/apps/meteor/app/2fa/server/functions/resetTOTP.ts @@ -22,7 +22,7 @@ const sendResetNotification = async function (uid: string): Promise { return; } - const t = (s: string): string => i18n.t(s, { lng: language }); + const t = i18n.getFixedT(language); const text = ` ${t('Your_TOTP_has_been_reset')} diff --git a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts index 73fbf6e51a04..119071e8ece0 100644 --- a/apps/meteor/app/lib/server/methods/addUsersToRoom.ts +++ b/apps/meteor/app/lib/server/methods/addUsersToRoom.ts @@ -98,14 +98,11 @@ export const addUsersToRoomMethod = async (userId: string, data: { rid: string; return; } void api.broadcast('notify.ephemeralMessage', userId, data.rid, { - msg: i18n.t( - 'Username_is_already_in_here', - { - postProcess: 'sprintf', - sprintf: [newUser.username], - }, - user?.language, - ), + msg: i18n.t('Username_is_already_in_here', { + postProcess: 'sprintf', + sprintf: [newUser.username], + lng: user?.language, + }), }); } }), diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 56009f15fede..e004f2199fa0 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -2,6 +2,7 @@ import { api } from '@rocket.chat/core-services'; import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, Users } from '@rocket.chat/models'; +import type { TOptions } from 'i18next'; import { check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; @@ -98,9 +99,9 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast void; @@ -9,7 +9,7 @@ type PlaceChatOnHoldModalProps = { }; const PlaceChatOnHoldModal = ({ onCancel, onOnHoldChat, confirm = onOnHoldChat, ...props }: PlaceChatOnHoldModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index 7aacfacb4476..404f576ea513 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -159,7 +159,7 @@ API.v1.addRoute( const visitorEmail = visitor.visitorEmails?.[0]?.address; const language = servingAgent.language || rcSettings.get('Language') || 'en'; - const t = (s: string): string => i18n.t(s, { lng: language }); + const t = i18n.getFixedT(language); const subject = t('Transcript_of_your_livechat_conversation'); options.emailTranscript = { diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts index 17f21d8d7b04..017eb516a487 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.ts +++ b/apps/meteor/app/livechat/server/lib/Helper.ts @@ -439,8 +439,8 @@ export const dispatchInquiryQueued = async (inquiry: ILivechatInquiryRecord, age hasMentionToHere: false, message: { _id: '', u: v, msg: '' }, // we should use server's language for this type of messages instead of user's - notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language), - room: Object.assign(room, { name: i18n.t('New_chat_in_queue', {}, language) }), + notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName, lng: language }), + room: Object.assign(room, { name: i18n.t('New_chat_in_queue', { lng: language }) }), mentionIds: [], }); } diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.ts b/apps/meteor/app/livechat/server/lib/QueueManager.ts index e1ea79d84163..c6728d470870 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.ts +++ b/apps/meteor/app/livechat/server/lib/QueueManager.ts @@ -371,8 +371,8 @@ export class QueueManager { hasMentionToHere: false, message: { _id: '', u: v, msg: '' }, // we should use server's language for this type of messages instead of user's - notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName }, language), - room: { ...room, name: i18n.t('New_chat_in_queue', {}, language) }, + notificationMessage: i18n.t('User_started_a_new_conversation', { username: notificationUserName, lng: language }), + room: { ...room, name: i18n.t('New_chat_in_queue', { lng: language }) }, mentionIds: [], }); } diff --git a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx index 420bf93df66d..21e502c90001 100644 --- a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx +++ b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx @@ -1,8 +1,8 @@ import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect } from 'react'; import { useForm, Controller } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../../../client/components/GenericModal'; @@ -13,7 +13,7 @@ type AddLinkComposerActionModalProps = { }; const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLinkComposerActionModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const textField = useUniqueId(); const urlField = useUniqueId(); diff --git a/apps/meteor/app/utils/lib/i18n.ts b/apps/meteor/app/utils/lib/i18n.ts index b69fe6b30513..737b98666d0a 100644 --- a/apps/meteor/app/utils/lib/i18n.ts +++ b/apps/meteor/app/utils/lib/i18n.ts @@ -1,4 +1,5 @@ import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; +import type { TOptions } from 'i18next'; import i18next from 'i18next'; import sprintf from 'i18next-sprintf-postprocessor'; @@ -13,7 +14,7 @@ export const addSprinfToI18n = function (t: (typeof i18n)['t']) { } if (isObject(replaces[0]) && !Array.isArray(replaces[0])) { - return t(key, replaces[0]); + return t(key, replaces[0] as TOptions); } return t(key, { diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx index af9b907df12e..2693060578ed 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmniChannelCallDialPad.tsx @@ -1,7 +1,7 @@ import { NavBarItem } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useVoipOutboundStates } from '../../contexts/CallContext'; import { useDialModal } from '../../hooks/useDialModal'; @@ -9,7 +9,7 @@ import { useDialModal } from '../../hooks/useDialModal'; type NavBarItemOmniChannelCallDialPadProps = ComponentPropsWithoutRef; const NavBarItemOmniChannelCallDialPad = (props: NavBarItemOmniChannelCallDialPadProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const { openDialModal } = useDialModal(); diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx index cf4e7ec240b4..7f2b6adc8691 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleError.tsx @@ -1,12 +1,12 @@ import { NavBarItem } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type NavBarItemOmnichannelCallToggleErrorProps = ComponentPropsWithoutRef; const NavBarItemOmnichannelCallToggleError = (props: NavBarItemOmnichannelCallToggleErrorProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ; }; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx index c4b53acefabb..149500050402 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleLoading.tsx @@ -1,12 +1,12 @@ import { NavBarItem } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type NavBarItemOmnichannelCallToggleLoadingProps = ComponentPropsWithoutRef; const NavBarItemOmnichannelCallToggleLoading = (props: NavBarItemOmnichannelCallToggleLoadingProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ; }; diff --git a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx index 8b51fc6c5b57..82f1c28350cd 100644 --- a/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx +++ b/apps/meteor/client/NavBarV2/NavBarOmnichannelToolbar/NavBarItemOmnichannelCallToggleReady.tsx @@ -1,14 +1,14 @@ import { NavBarItem } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React, { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useCallerInfo, useCallRegisterClient, useCallUnregisterClient, useVoipNetworkStatus } from '../../contexts/CallContext'; type NavBarItemOmnichannelCallToggleReadyProps = ComponentPropsWithoutRef; const NavBarItemOmnichannelCallToggleReady = (props: NavBarItemOmnichannelCallToggleReadyProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const caller = useCallerInfo(); const unregister = useCallUnregisterClient(); diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx index 22895d55388f..149ad0ea585e 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/UserMenu.tsx @@ -1,9 +1,9 @@ import type { IUser } from '@rocket.chat/core-typings'; import { GenericMenu, useHandleMenuAction } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import UserMenuButton from './UserMenuButton'; import { useUserMenu } from './hooks/useUserMenu'; @@ -11,7 +11,7 @@ import { useUserMenu } from './hooks/useUserMenu'; type UserMenuProps = { user: IUser } & Omit, 'sections' | 'items' | 'title'>; const UserMenu = function UserMenu({ user, ...props }: UserMenuProps) { - const t = useTranslation(); + const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const sections = useUserMenu(user); diff --git a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx index f589dd21ed50..dbaea02ace4a 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterContainer.tsx @@ -1,7 +1,7 @@ import { Avatar } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { ContextualbarTitle, @@ -19,7 +19,7 @@ interface IGameCenterContainerProps { } const GameCenterContainer = ({ handleClose, handleBack, game }: IGameCenterContainerProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); return ( <> diff --git a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx index d0dcc6fad4fe..871e82f3ff56 100644 --- a/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx +++ b/apps/meteor/client/apps/gameCenter/GameCenterInvitePlayersModal.tsx @@ -1,8 +1,8 @@ import type { IUser } from '@rocket.chat/core-typings'; import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../../components/GenericModal'; import UserAutoCompleteMultipleFederated from '../../components/UserAutoCompleteMultiple/UserAutoCompleteMultipleFederated'; @@ -19,7 +19,7 @@ interface IGameCenterInvitePlayersModalProps { } const GameCenterInvitePlayersModal = ({ game, onClose }: IGameCenterInvitePlayersModalProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const [users, setUsers] = useState>([]); const { name } = game; diff --git a/apps/meteor/client/components/ActionManagerBusyState.tsx b/apps/meteor/client/components/ActionManagerBusyState.tsx index 1399c045271f..932eb08ea502 100644 --- a/apps/meteor/client/components/ActionManagerBusyState.tsx +++ b/apps/meteor/client/components/ActionManagerBusyState.tsx @@ -1,12 +1,12 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useUiKitActionManager } from '../uikit/hooks/useUiKitActionManager'; const ActionManagerBusyState = () => { - const t = useTranslation(); + const { t } = useTranslation(); const actionManager = useUiKitActionManager(); const [busy, setBusy] = useState(false); diff --git a/apps/meteor/client/components/AutoCompleteDepartment.tsx b/apps/meteor/client/components/AutoCompleteDepartment.tsx index 6217f1d99610..0c50f2254aac 100644 --- a/apps/meteor/client/components/AutoCompleteDepartment.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartment.tsx @@ -1,8 +1,8 @@ import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { memo, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useRecordList } from '../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../hooks/useAsyncState'; @@ -28,7 +28,7 @@ const AutoCompleteDepartment = ({ showArchived = false, ...props }: AutoCompleteDepartmentProps): ReactElement | null => { - const t = useTranslation(); + const { t } = useTranslation(); const [departmentsFilter, setDepartmentsFilter] = useState(''); const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); diff --git a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx index 99af9a1f6a2c..c15d480f3900 100644 --- a/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx +++ b/apps/meteor/client/components/AutoCompleteDepartmentMultiple.tsx @@ -1,9 +1,9 @@ import { CheckOption, PaginatedMultiSelectFiltered } from '@rocket.chat/fuselage'; import type { PaginatedMultiSelectOption } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React, { memo, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; import { useRecordList } from '../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../hooks/useAsyncState'; @@ -24,7 +24,7 @@ const AutoCompleteDepartmentMultiple = ({ enabled = false, onChange = () => undefined, }: AutoCompleteDepartmentMultipleProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const [departmentsFilter, setDepartmentsFilter] = useState(''); const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500); diff --git a/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx b/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx index 349341baf003..77135fad6230 100644 --- a/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx +++ b/apps/meteor/client/components/ConfirmOwnerChangeModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentPropsWithoutRef } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from './GenericModal'; import RawText from './RawText'; @@ -20,7 +20,7 @@ const ConfirmOwnerChangeModal = ({ onConfirm, onCancel, }: ConfirmOwnerChangeModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); let changeOwnerRooms = ''; if (shouldChangeOwner.length > 0) { diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx index c8e17ab88d80..dcac448b1e92 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarBack.tsx @@ -1,13 +1,13 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ComponentProps } from 'react'; import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import ContextualbarAction from './ContextualbarAction'; type ContextualbarBackProps = Partial>; const ContextualbarBack = (props: ContextualbarBackProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); return ; }; diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx index 1670c9be5895..38db516476e3 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarClose.tsx @@ -1,13 +1,13 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import ContextualbarAction from './ContextualbarAction'; type ContextualbarCloseProps = Partial>; const ContextualbarClose = (props: ContextualbarCloseProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); return ; }; diff --git a/apps/meteor/client/components/FilterByText.tsx b/apps/meteor/client/components/FilterByText.tsx index 5c5a3d599e2f..25d8e225e3d8 100644 --- a/apps/meteor/client/components/FilterByText.tsx +++ b/apps/meteor/client/components/FilterByText.tsx @@ -1,8 +1,8 @@ import { Box, Icon, TextInput, Margins } from '@rocket.chat/fuselage'; import { useAutoFocus, useMergedRefs } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, FormEvent, HTMLAttributes } from 'react'; import React, { forwardRef, memo, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; type FilterByTextProps = { onChange: (filter: string) => void; @@ -13,7 +13,7 @@ const FilterByText = forwardRef(function Fi { placeholder, onChange: setFilter, shouldAutoFocus = false, children, ...props }, ref, ) { - const t = useTranslation(); + const { t } = useTranslation(); const [text, setText] = useState(''); const autoFocusRef = useAutoFocus(shouldAutoFocus); const mergedRefs = useMergedRefs(ref, autoFocusRef); diff --git a/apps/meteor/client/components/FingerprintChangeModal.tsx b/apps/meteor/client/components/FingerprintChangeModal.tsx index db4c33654a92..a45a17db8ccc 100644 --- a/apps/meteor/client/components/FingerprintChangeModal.tsx +++ b/apps/meteor/client/components/FingerprintChangeModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from './GenericModal'; @@ -12,7 +12,7 @@ type FingerprintChangeModalProps = { }; const FingerprintChangeModal = ({ onConfirm, onCancel, onClose }: FingerprintChangeModalProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); return ( { - const t = useTranslation(); + const { t } = useTranslation(); return ( { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index 5d025e05827d..d91e3c066007 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -1,9 +1,9 @@ import { Button, Modal } from '@rocket.chat/fuselage'; import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; import type { Keys as IconName } from '@rocket.chat/icons'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, ReactNode, ComponentPropsWithoutRef } from 'react'; import React, { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import type { RequiredModalProps } from './withDoNotAskAgain'; import { withDoNotAskAgain } from './withDoNotAskAgain'; @@ -75,7 +75,7 @@ const GenericModal = ({ annotation, ...props }: GenericModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const genericModalId = useUniqueId(); const dismissedRef = useRef(true); diff --git a/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx b/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx index 3fcfe2b0e0ac..d21023024fb3 100644 --- a/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx +++ b/apps/meteor/client/components/GenericNoResults/GenericNoResults.tsx @@ -1,7 +1,7 @@ import { Box, States, StatesIcon, StatesLink, StatesTitle, StatesSubtitle, StatesActions, StatesAction } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type LinkProps = { linkText: string; linkHref: string } | { linkText?: never; linkHref?: never }; type ButtonProps = { buttonTitle: string; buttonAction: () => void } | { buttonTitle?: never; buttonAction?: never }; @@ -23,7 +23,7 @@ const GenericNoResults = ({ linkHref, linkText, }: GenericNoResultsProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts b/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts index 0a8a8deb8262..4c390041497e 100644 --- a/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts +++ b/apps/meteor/client/components/GenericTable/hooks/useItemsPerPageLabel.ts @@ -1,7 +1,7 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; export const useItemsPerPageLabel = (): (() => string) => { - const t = useTranslation(); + const { t } = useTranslation(); return useCallback(() => t('Items_per_page:'), [t]); }; diff --git a/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts b/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts index c610340f28bd..8ff7d2ac18cf 100644 --- a/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts +++ b/apps/meteor/client/components/GenericTable/hooks/useShowingResultsLabel.ts @@ -1,15 +1,19 @@ import type { Pagination } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; type Props['showingResultsLabel'] = ComponentProps['showingResultsLabel']> = T extends (...args: any[]) => any ? Parameters : never; export const useShowingResultsLabel = (): ((...params: Props) => string) => { - const t = useTranslation(); + const { t } = useTranslation(); return useCallback( - ({ count, current, itemsPerPage }) => t('Showing_results_of', current + 1, Math.min(current + itemsPerPage, count), count), + ({ count, current, itemsPerPage }) => + t('Showing_results_of', { + postProcess: 'sprintf', + sprintf: [current + 1, Math.min(current + itemsPerPage, count), count], + }), [t], ); }; diff --git a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx index 3d68e3f4b6d3..e7ce515ac496 100644 --- a/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx +++ b/apps/meteor/client/components/GenericUpsellModal/GenericUpsellModal.tsx @@ -1,8 +1,8 @@ import { Box, Button, Modal } from '@rocket.chat/fuselage'; import type { Keys as IconName } from '@rocket.chat/icons'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ReactElement, ComponentProps } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type GenericUpsellModalProps = { children?: ReactNode; @@ -35,7 +35,7 @@ const GenericUpsellModal = ({ annotation, ...props }: GenericUpsellModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx index 2cabfed460bd..6db61b2ec910 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGallery.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGallery.tsx @@ -1,10 +1,10 @@ import type { IUpload } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Box, ButtonGroup, IconButton, Palette, Throbber } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useRef, useState } from 'react'; import { FocusScope } from 'react-aria'; import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; import { Keyboard, Navigation, Zoom, A11y } from 'swiper'; import type { SwiperRef } from 'swiper/react'; import { type SwiperClass, Swiper, SwiperSlide } from 'swiper/react'; @@ -108,7 +108,7 @@ const swiperStyle = css` `; export const ImageGallery = ({ images, onClose, loadMore }: { images: IUpload[]; onClose: () => void; loadMore?: () => void }) => { - const t = useTranslation(); + const { t } = useTranslation(); const swiperRef = useRef(null); const [, setSwiperInst] = useState(); const [zoomScale, setZoomScale] = useState(1); diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx index 97d91de95f62..8dcc55a93a48 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryError.tsx @@ -1,8 +1,8 @@ import { css } from '@rocket.chat/css-in-js'; import { IconButton, ModalBackdrop } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; import GenericError from '../GenericError/GenericError'; @@ -14,7 +14,7 @@ const closeButtonStyle = css` `; export const ImageGalleryError = ({ onClose }: { onClose: () => void }) => { - const t = useTranslation(); + const { t } = useTranslation(); return createPortal( diff --git a/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx b/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx index 1c057584bd1f..588605786664 100644 --- a/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx +++ b/apps/meteor/client/components/ImageGallery/ImageGalleryLoading.tsx @@ -1,8 +1,8 @@ import { css } from '@rocket.chat/css-in-js'; import { IconButton, ModalBackdrop, Throbber } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; import { createPortal } from 'react-dom'; +import { useTranslation } from 'react-i18next'; const closeButtonStyle = css` position: absolute; @@ -12,7 +12,7 @@ const closeButtonStyle = css` `; export const ImageGalleryLoading = ({ onClose }: { onClose: () => void }) => { - const t = useTranslation(); + const { t } = useTranslation(); return createPortal( diff --git a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx index cbefeb2c72c1..6f94be4ebc90 100644 --- a/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx +++ b/apps/meteor/client/components/InfoPanel/RetentionPolicyCallout.tsx @@ -1,14 +1,14 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { Callout } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { usePruneWarningMessage } from '../../hooks/usePruneWarningMessage'; import { withErrorBoundary } from '../withErrorBoundary'; const RetentionPolicyCallout = ({ room }: { room: IRoom }) => { const message = usePruneWarningMessage(room); - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/LocalTime.tsx b/apps/meteor/client/components/LocalTime.tsx index 100ba2ec6a62..498b2da9711e 100644 --- a/apps/meteor/client/components/LocalTime.tsx +++ b/apps/meteor/client/components/LocalTime.tsx @@ -1,6 +1,6 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useUTCClock } from '../hooks/useUTCClock'; @@ -10,7 +10,7 @@ type LocalTimeProps = { const LocalTime = ({ utcOffset }: LocalTimeProps): ReactElement => { const time = useUTCClock(utcOffset); - const t = useTranslation(); + const { t } = useTranslation(); return <>{t('Local_Time_time', { time })}; }; diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index c9af942f6e1c..6426b24810ee 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -1,10 +1,10 @@ import { Box } from '@rocket.chat/fuselage'; import { isExternal, getBaseURI } from '@rocket.chat/ui-client'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import dompurify from 'dompurify'; import { marked } from 'marked'; import type { ComponentProps } from 'react'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { renderMessageEmoji } from '../lib/utils/renderMessageEmoji'; @@ -89,7 +89,7 @@ const MarkdownText = ({ ...props }: MarkdownTextProps) => { const sanitizer = dompurify.sanitize; - const t = useTranslation(); + const { t } = useTranslation(); let markedOptions: marked.MarkedOptions; const schemes = 'http,https,notes,ftp,ftps,tel,mailto,sms,cid'; diff --git a/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx index 04fcb29eed01..b19ccfee1769 100644 --- a/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/ReturnChatQueueModal.tsx @@ -1,6 +1,6 @@ import { Button, Modal } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type ReturnChatQueueModalProps = { onMoveChat: () => void; @@ -8,7 +8,7 @@ type ReturnChatQueueModalProps = { }; const ReturnChatQueueModal = ({ onCancel, onMoveChat, ...props }: ReturnChatQueueModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx index c06b6a190465..502d0f9d1af7 100644 --- a/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/TranscriptModal.tsx @@ -1,8 +1,8 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; import { Field, Button, TextInput, Modal, Box, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useCallback, useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; type TranscriptModalProps = { email: string; @@ -14,7 +14,7 @@ type TranscriptModalProps = { }; const TranscriptModal = ({ email: emailDefault = '', room, onRequest, onSend, onCancel, onDiscard, ...props }: TranscriptModalProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const { register, diff --git a/apps/meteor/client/components/Sidebar/Header.tsx b/apps/meteor/client/components/Sidebar/Header.tsx index e4bf5a5e7041..dcbf7f1cb505 100644 --- a/apps/meteor/client/components/Sidebar/Header.tsx +++ b/apps/meteor/client/components/Sidebar/Header.tsx @@ -1,7 +1,7 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type HeaderProps = { children?: ReactNode; @@ -10,7 +10,7 @@ type HeaderProps = { }; const Header = ({ title, onClose, children, ...props }: HeaderProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx b/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx index 45eb6094572a..b588e223d922 100644 --- a/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx +++ b/apps/meteor/client/components/Sidebar/SidebarItemsAssembler.tsx @@ -1,6 +1,6 @@ import { Divider } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { Fragment, memo } from 'react'; +import { useTranslation } from 'react-i18next'; import type { SidebarItem } from '../../lib/createSidebarItems'; import { isSidebarItem } from '../../lib/createSidebarItems'; @@ -12,7 +12,7 @@ type SidebarItemsAssemblerProps = { }; const SidebarItemsAssembler = ({ items, currentPath }: SidebarItemsAssemblerProps) => { - const t = useTranslation(); + const { t, i18n } = useTranslation(); return ( <> @@ -25,7 +25,7 @@ const SidebarItemsAssembler = ({ items, currentPath }: SidebarItemsAssemblerProp icon={props.icon} label={t((props.i18nLabel || props.name) as Parameters[0])} currentPath={currentPath} - tag={props.tag && t.has(props.tag) ? t(props.tag) : props.tag} + tag={props.tag && i18n.exists(props.tag) ? t(props.tag) : props.tag} externalUrl={props.externalUrl} badge={props.badge} /> diff --git a/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx b/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx index cd0c25f03d86..1eefc50baabd 100644 --- a/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx +++ b/apps/meteor/client/components/SidebarToggler/SidebarTogglerButton.tsx @@ -1,6 +1,6 @@ import { Box, IconButton } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import SidebarTogglerBadge from './SidebarTogglerBadge'; @@ -10,7 +10,7 @@ type SideBarTogglerButtonProps = { }; const SideBarTogglerButton = ({ badge, onClick }: SideBarTogglerButtonProps) => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/TextCopy.tsx b/apps/meteor/client/components/TextCopy.tsx index 467e954ddd65..f9e2ffc62284 100644 --- a/apps/meteor/client/components/TextCopy.tsx +++ b/apps/meteor/client/components/TextCopy.tsx @@ -1,7 +1,7 @@ import { Box, Button, Scrollable } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import useClipboardWithToast from '../hooks/useClipboardWithToast'; @@ -17,7 +17,7 @@ type TextCopyProps = { } & ComponentProps; const TextCopy = ({ text, wrapper = defaultWrapperRenderer, ...props }: TextCopyProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const { copy } = useClipboardWithToast(text); diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx index 4c91e274de68..4867d171aee8 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorPasswordModal.tsx @@ -1,8 +1,8 @@ import { Box, PasswordInput, FieldGroup, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, Ref, SyntheticEvent } from 'react'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../GenericModal'; import type { OnConfirm } from './TwoFactorModal'; @@ -15,7 +15,7 @@ type TwoFactorPasswordModalProps = { }; const TwoFactorPasswordModal = ({ onConfirm, onClose, invalidAttempt }: TwoFactorPasswordModalProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const [code, setCode] = useState(''); const ref = useAutoFocus(); diff --git a/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx b/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx index 6f36c9c8ce26..d6f167be8588 100644 --- a/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx +++ b/apps/meteor/client/components/TwoFactorModal/TwoFactorTotpModal.tsx @@ -1,8 +1,8 @@ import { Box, TextInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; import { useAutoFocus, useUniqueId } from '@rocket.chat/fuselage-hooks'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ChangeEvent, SyntheticEvent } from 'react'; import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from '../GenericModal'; import type { OnConfirm } from './TwoFactorModal'; @@ -15,7 +15,7 @@ type TwoFactorTotpModalProps = { }; const TwoFactorTotpModal = ({ onConfirm, onClose, invalidAttempt }: TwoFactorTotpModalProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const [code, setCode] = useState(''); const ref = useAutoFocus(); diff --git a/apps/meteor/client/components/UrlChangeModal.tsx b/apps/meteor/client/components/UrlChangeModal.tsx index 13d0523c92aa..dbc152c7fbff 100644 --- a/apps/meteor/client/components/UrlChangeModal.tsx +++ b/apps/meteor/client/components/UrlChangeModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import GenericModal from './GenericModal'; @@ -13,14 +13,17 @@ type UrlChangeModalProps = { }; const UrlChangeModal = ({ onConfirm, siteUrl, currentUrl, onClose }: UrlChangeModalProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); return (

diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index bf143229cf65..98e1cce2ab78 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -1,9 +1,9 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Button, IconButton } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ComponentProps } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; import { useEmbeddedLayout } from '../../hooks/useEmbeddedLayout'; import MarkdownText from '../MarkdownText'; @@ -52,7 +52,7 @@ const UserCard = ({ nickname, ...props }: UserCardProps) => { - const t = useTranslation(); + const { t } = useTranslation(); const isLayoutEmbedded = useEmbeddedLayout(); return ( diff --git a/apps/meteor/client/components/UserInfo/UserInfo.tsx b/apps/meteor/client/components/UserInfo/UserInfo.tsx index a7f32f82f454..ac879d21738b 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.tsx @@ -1,9 +1,9 @@ import type { IUser, Serialized } from '@rocket.chat/core-typings'; import { Box, Margins, Tag } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React, { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useTimeAgo } from '../../hooks/useTimeAgo'; import { useUserCustomFields } from '../../hooks/useUserCustomFields'; @@ -72,7 +72,7 @@ const UserInfo = ({ reason, ...props }: UserInfoProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); const timeAgo = useTimeAgo(); const userDisplayName = useUserDisplayName({ name, username }); const userCustomFields = useUserCustomFields(customFields); diff --git a/apps/meteor/client/components/WarningModal.tsx b/apps/meteor/client/components/WarningModal.tsx index 00ae92e1d3bd..db0697f78c0d 100644 --- a/apps/meteor/client/components/WarningModal.tsx +++ b/apps/meteor/client/components/WarningModal.tsx @@ -1,7 +1,7 @@ import { Button, Modal } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, ReactNode } from 'react'; import React from 'react'; +import { useTranslation } from 'react-i18next'; type WarningModalProps = { text: ReactNode; @@ -13,7 +13,7 @@ type WarningModalProps = { }; const WarningModal = ({ text, confirmText, close, cancel, cancelText, confirm, ...props }: WarningModalProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); return ( diff --git a/apps/meteor/client/components/dashboards/PeriodSelector.tsx b/apps/meteor/client/components/dashboards/PeriodSelector.tsx index 6977e3334f4f..d441fbf2f19a 100644 --- a/apps/meteor/client/components/dashboards/PeriodSelector.tsx +++ b/apps/meteor/client/components/dashboards/PeriodSelector.tsx @@ -1,7 +1,7 @@ import { Select } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import type { Period } from './periods'; import { getPeriod } from './periods'; @@ -14,9 +14,9 @@ type PeriodSelectorProps = { }; const PeriodSelector = ({ periods, value, name, onChange }: PeriodSelectorProps): ReactElement => { - const t = useTranslation(); + const { t } = useTranslation(); - const options = useMemo<[string, string][]>(() => periods.map((period) => [period, t(...getPeriod(period).label)]), [periods, t]); + const options = useMemo<[string, string][]>(() => periods.map((period) => [period, t(getPeriod(period).label)]), [periods, t]); return ( ( ( validateChannelName(value), })} error={errors.name?.message} diff --git a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx index ad1abdcbf399..3543234bca96 100644 --- a/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebar/header/CreateTeam/CreateTeamModal.tsx @@ -181,7 +181,7 @@ const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => id={nameId} aria-invalid={errors.name ? 'true' : 'false'} {...register('name', { - required: t('error-the-field-is-required', { field: t('Name') }), + required: t('Required_field', { field: t('Name') }), validate: (value) => validateTeamName(value), })} addon={} diff --git a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx b/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx index 38a1df5a5ec8..2ff468f4f72b 100644 --- a/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateChannelModal.tsx @@ -222,7 +222,7 @@ const CreateChannelModal = ({ teamId = '', onClose, reload }: CreateChannelModal id={nameId} data-qa-type='channel-name-input' {...register('name', { - required: t('error-the-field-is-required', { field: t('Name') }), + required: t('Required_field', { field: t('Name') }), validate: (value) => validateChannelName(value), })} error={errors.name?.message} diff --git a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx index e907558ce278..a7e7b506de0f 100644 --- a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx @@ -183,7 +183,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { id={nameId} aria-invalid={errors.name ? 'true' : 'false'} {...register('name', { - required: t('error-the-field-is-required', { field: t('Name') }), + required: t('Required_field', { field: t('Name') }), validate: (value) => validateTeamName(value), })} addon={} diff --git a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx index 2c11b7a384cd..ce9fad8591fa 100644 --- a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx +++ b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx @@ -1,6 +1,6 @@ import type { SelectOption } from '@rocket.chat/fuselage'; -import { SelectLegacy, Box, Button, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage'; -import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { SelectLegacy, Box, Button, Field, FieldLabel, FieldRow, FieldError } from '@rocket.chat/fuselage'; +import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; @@ -13,7 +13,11 @@ import { useRemoveWebDAVAccountIntegrationMutation } from './hooks/useRemoveWebD const AccountIntegrationsPage = () => { const { data: webdavAccountIntegrations } = useWebDAVAccountIntegrationsQuery(); - const { handleSubmit, control } = useForm<{ accountSelected: string }>(); + const { + handleSubmit, + control, + formState: { errors }, + } = useForm<{ accountSelected: string }>(); const options: SelectOption[] = useMemo( () => webdavAccountIntegrations?.map(({ _id, ...current }) => [_id, getWebdavServerName(current)]) ?? [], @@ -36,6 +40,8 @@ const AccountIntegrationsPage = () => { removeMutation.mutate({ accountSelected }); }); + const accountSelectedId = useUniqueId(); + return ( @@ -47,22 +53,18 @@ const AccountIntegrationsPage = () => { ( - - )} + rules={{ required: t('Required_field', { field: t('WebDAV_Accounts') }) }} + render={({ field }) => } /> + {errors?.accountSelected && ( + + {errors.accountSelected.message} + + )} diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index 0de462cd5bd3..95398b352049 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -166,7 +166,9 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle (requireName && name === '' ? t('error-the-field-is-required', { field: t('Name') }) : true) }} + rules={{ + required: requireName && t('Required_field', { field: t('Name') }), + }} render={({ field }) => ( ): ReactEle control={control} name='username' rules={{ - required: t('error-the-field-is-required', { field: t('Username') }), + required: t('Required_field', { field: t('Username') }), validate: (username) => validateUsername(username), }} render={({ field }) => ( @@ -305,7 +307,10 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle (validateEmail(email) ? undefined : t('error-invalid-email-address')) } }} + rules={{ + required: t('Required_field', { field: t('Email') }), + validate: { validateEmail: (email) => (validateEmail(email) ? undefined : t('error-invalid-email-address')) }, + }} render={({ field }) => ( { const methods = useForm({ defaultValues: passwordDefaultValues, - mode: 'onBlur', + mode: 'all', }); const { reset, diff --git a/apps/meteor/client/views/account/security/ChangePassword.tsx b/apps/meteor/client/views/account/security/ChangePassword.tsx index c70d9e166175..e5cb61e34547 100644 --- a/apps/meteor/client/views/account/security/ChangePassword.tsx +++ b/apps/meteor/client/views/account/security/ChangePassword.tsx @@ -53,13 +53,13 @@ const ChangePassword = (props: AllHTMLAttributes) => { control={control} name='password' rules={{ + required: t('Required_field', { field: t('New_password') }), validate: () => (password?.length && !passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), }} - render={({ field: { onChange, value } }) => ( + render={({ field }) => ( } @@ -84,12 +84,14 @@ const ChangePassword = (props: AllHTMLAttributes) => { (password !== confirmationPassword ? t('Passwords_do_not_match') : true) }} - render={({ field: { onChange, value } }) => ( + rules={{ + required: t('Required_field', { field: t('Confirm_password') }), + validate: (confirmationPassword) => (password !== confirmationPassword ? t('Passwords_do_not_match') : true), + }} + render={({ field }) => ( } diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 72213f3202ba..b2fb982ebb9b 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,8 +1,9 @@ import { Box, Margins, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useMethod, useTranslation, useLogout } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; import React, { useCallback, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { e2e } from '../../../../app/e2e/client/rocketchat.e2e'; @@ -17,17 +18,17 @@ const EndToEnd = (props: ComponentProps): ReactElement => { const resetE2eKey = useMethod('e2e.resetOwnE2EKey'); const { - register, handleSubmit, watch, resetField, formState: { errors, isValid }, + control, } = useForm({ defaultValues: { password: '', passwordConfirm: '', }, - mode: 'onChange', + mode: 'all', }); const { password } = watch(); @@ -64,37 +65,71 @@ const EndToEnd = (props: ComponentProps): ReactElement => { } }, [password, resetField]); + const passwordId = useUniqueId(); + const e2ePasswordExplanationId = useUniqueId(); + const passwordConfirmId = useUniqueId(); + return ( {t('E2E_Encryption_Password_Change')} - + - {t('New_encryption_password')} + {t('New_encryption_password')} - ( + + )} /> - {!keysExist && {t('EncryptionKey_Change_Disabled')}} + {!keysExist && {t('EncryptionKey_Change_Disabled')}} + {errors?.password && ( + + {errors.password.message} + + )} {hasTypedPassword && ( - {t('Confirm_new_encryption_password')} - (password !== value ? 'Your passwords do no match' : true), - })} - placeholder={t('Confirm_New_Password_Placeholder')} - aria-labelledby='Confirm_new_encryption_password' - /> - {errors.passwordConfirm && {errors.passwordConfirm.message}} + {t('Confirm_new_encryption_password')} + + (password !== value ? 'Your passwords do no match' : true), + }} + render={({ field }) => ( + + )} + /> + + {errors.passwordConfirm && ( + + {errors.passwordConfirm.message} + + )} )} diff --git a/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx b/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx index 59da79508192..f6f52507d33a 100644 --- a/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx +++ b/apps/meteor/client/views/admin/customEmoji/AddCustomEmoji.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, FieldError, Icon } from '@rocket.chat/fuselage'; +import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldRow, FieldError, IconButton } from '@rocket.chat/fuselage'; import type { ReactElement, ChangeEvent } from 'react'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -82,7 +82,7 @@ const AddCustomEmoji = ({ close, onChange, ...props }: AddCustomEmojiProps): Rea - {errors.name && {t('error-the-field-is-required', { field: t('Name') })}} + {errors.name && {t('Required_field', { field: t('Name') })}} {t('Aliases')} @@ -94,12 +94,9 @@ const AddCustomEmoji = ({ close, onChange, ...props }: AddCustomEmojiProps): Rea {t('Custom_Emoji')} - {/* FIXME: replace to IconButton */} - + - {errors.emoji && {t('error-the-field-is-required', { field: t('Custom_Emoji') })}} + {errors.emoji && {t('Required_field', { field: t('Custom_Emoji') })}} {newEmojiPreview && ( diff --git a/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx b/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx index 8f63b2ac7b48..49ee12b69dd0 100644 --- a/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx +++ b/apps/meteor/client/views/admin/customEmoji/EditCustomEmoji.tsx @@ -150,7 +150,7 @@ const EditCustomEmoji = ({ close, onChange, data, ...props }: EditCustomEmojiPro - {errors.name && {t('error-the-field-is-required', { field: t('Name') })}} + {errors.name && {t('Required_field', { field: t('Name') })}} {t('Aliases')} diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 82f668a2d7d5..430d05ceaa53 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -1,4 +1,4 @@ -import { Field, FieldLabel, FieldRow, TextInput, Box, Icon, Margins, Button, ButtonGroup } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, TextInput, Box, Margins, Button, ButtonGroup, IconButton } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement, FormEvent } from 'react'; import React, { useState, useCallback } from 'react'; @@ -34,8 +34,8 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr const soundData = createSoundData(soundFile, name); const validation = validate(soundData, soundFile) as Array[0]>; - validation.forEach((error) => { - throw new Error(t('error-the-field-is-required', { field: t(error) })); + validation.forEach((invalidFieldName) => { + throw new Error(t('Required_field', { field: t(invalidFieldName) })); }); try { @@ -97,12 +97,9 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr {t('Sound_File_mp3')} - + - {/* FIXME: replace to IconButton */} - + {sound?.name || t('None')} @@ -111,7 +108,7 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr - diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index b358d0bb7c84..1274e9deda35 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -76,10 +76,10 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl } } - validation.forEach((error) => + validation.forEach((invalidFieldName) => dispatchToastMessage({ type: 'error', - message: t('error-the-field-is-required', { field: t(error) }), + message: t('Required_field', { field: t(invalidFieldName) }), }), ); }, @@ -131,9 +131,9 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl {t('Sound_File_mp3')} - + - + {sound?.name || 'none'} diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx index 78c2618b4c53..07376208aaef 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusForm.tsx @@ -28,9 +28,10 @@ const CustomUserStatusForm = ({ onClose, onReload, status }: CustomUserStatusFor register, control, handleSubmit, - formState: { isDirty, errors }, + formState: { errors }, } = useForm({ defaultValues: { name: status?.name ?? '', statusType: status?.statusType ?? '' }, + mode: 'all', }); const saveStatus = useEndpoint('POST', _id ? '/v1/custom-user-status.update' : '/v1/custom-user-status.create'); @@ -94,9 +95,9 @@ const CustomUserStatusForm = ({ onClose, onReload, status }: CustomUserStatusFor {t('Name')} - + - {errors?.name && {t('error-the-field-is-required', { field: t('Name') })}} + {errors.name && {errors.name.message}} {t('Presence')} @@ -104,18 +105,18 @@ const CustomUserStatusForm = ({ onClose, onReload, status }: CustomUserStatusFor )} /> - {errors.accountId && {t('Field_required')}} + {errors.accountId && {errors.accountId.message}} )} diff --git a/apps/meteor/client/views/root/MainLayout/RegisterUsername.tsx b/apps/meteor/client/views/root/MainLayout/RegisterUsername.tsx index bfbbce5d37e1..fd1691bcb16d 100644 --- a/apps/meteor/client/views/root/MainLayout/RegisterUsername.tsx +++ b/apps/meteor/client/views/root/MainLayout/RegisterUsername.tsx @@ -105,7 +105,10 @@ const RegisterUsername = () => { {t('Username')} - + {errors.username && ( diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts index d0e7b7133423..d3c667e20f82 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-center.spec.ts @@ -42,7 +42,7 @@ const URL = { }; const ERROR = { - nameRequired: 'The field Name is required.', + nameRequired: 'Name required', invalidEmail: 'Invalid email address', existingEmail: 'Email already exists', existingPhone: 'Phone already exists', diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts index 872eafdfb2a2..023d11de4757 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-departaments.spec.ts @@ -8,8 +8,8 @@ import { createDepartment, deleteDepartment } from '../utils/omnichannel/departm import { test, expect } from '../utils/test'; const ERROR = { - requiredName: 'The field name is required.', - requiredEmail: 'The field email is required.', + requiredName: 'Name required', + requiredEmail: 'Email required', invalidEmail: 'Invalid email address', }; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts index 1da1837572e5..9ee1d8d99fd9 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-priorities.spec.ts @@ -8,7 +8,7 @@ import { test, expect } from '../utils/test'; const PRIORITY_NAME = faker.person.firstName(); const ERROR = { - fieldNameRequired: 'The field Name is required.', + fieldNameRequired: 'Name required', }; test.skip(!IS_EE, 'Omnichannel Priorities > Enterprise Only'); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts index 35a371c61ba6..d5b5544e9ace 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies.spec.ts @@ -6,8 +6,8 @@ import { OmnichannelSlaPolicies } from '../page-objects/omnichannel-sla-policies import { test, expect } from '../utils/test'; const ERROR = { - nameRequired: 'The field Name is required.', - estimatedWaitTimeRequired: 'The field Estimated wait time (time in minutes) is required.', + nameRequired: 'Name required', + estimatedWaitTimeRequired: 'Estimated wait time (time in minutes) required', }; const INITIAL_SLA = { diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index 40ab367489b6..661360c109b2 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -2367,7 +2367,6 @@ "The_application_name_is_required": "Die aansoek naam is nodig", "The_channel_name_is_required": "Die kanaal naam is nodig", "The_emails_are_being_sent": "Die e-posse word gestuur.", - "The_field_is_required": "Die veld%s is nodig.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Die grootte van die prentjie sal nie werk nie omdat ons ImageMagick of GraphicsMagick nie op u bediener geïnstalleer het nie.", "The_redirectUri_is_required": "Die redirectUri is nodig", "The_server_will_restart_in_s_seconds": "Die bediener sal herbegin in%s sekondes", diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index d3a86879f716..6d3d406e6c81 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -4126,8 +4126,7 @@ "The_application_name_is_required": "اسم التطبيق مطلوب", "The_channel_name_is_required": "اسم القناة مطلوب", "The_emails_are_being_sent": "يتم إرسال رسائل البريد الإلكتروني.", - "The_empty_room__roomName__will_be_removed_automatically": "ستتم إزالة الغرفة الفارغة {{roomName}} will be تلقائيًا.", - "The_field_is_required": "الحقل %s مطلوب.", + "The_empty_room__roomName__will_be_removed_automatically": "ستتم إزالة الغرفة الفارغة {{roomName}} will be تلقائيًا.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "لن يعمل تغيير حجم الصورة لأننا لا نستطيع اكتشاف تثبيت ImageMagick أو GraphicsMagick على الخادم الخاص بك.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "الرسالة عبارة عن مناقشة أنك لن تتمكن من استعادة الرسائل!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "تم تعطيل إشعارات الهاتف المحمول لجميع المستخدمين، انتقل إلى \"المسؤول > منبثق\" لتمكين البوابة المنبثقة مرة أخرى", diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index 118be4b37920..7b1c3f48fa1c 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -2366,8 +2366,7 @@ "Thank_you_for_your_feedback": "Əlaqə üçün təşəkkür edirik", "The_application_name_is_required": "Ərizə adı tələb olunur", "The_channel_name_is_required": "Kanal adı tələb olunur", - "The_emails_are_being_sent": "E-poçt göndərilir.", - "The_field_is_required": "%s sahə tələb olunur.", + "The_emails_are_being_sent": "E-poçt göndərilir.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "ImageMagick və ya GraphicsMagick-i serverinizdə yükləməyəcəyimiz üçün şəkil ölçüsünün işləməyəcəyi.", "The_redirectUri_is_required": "RedirectUri tələb olunur", "The_server_will_restart_in_s_seconds": "Server%s saniyə ərzində yenidən başlayacaq", diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index 858688468755..daea286edfed 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -2384,8 +2384,7 @@ "Thank_you_for_your_feedback": "Дзякуй за ваш водгук", "The_application_name_is_required": "Імя прыкладання патрабуецца", "The_channel_name_is_required": "Назва канала патрабуецца", - "The_emails_are_being_sent": "Лісты былі адпраўленыя.", - "The_field_is_required": "Поле %s патрабуецца.", + "The_emails_are_being_sent": "Лісты былі адпраўленыя.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Змяніць памер малюнка не будзе працаваць, таму што мы не можам выявіць ImageMagick або GraphicsMagick усталяваны на вашым серверы.", "The_redirectUri_is_required": "RedirectUri патрабуецца", "The_server_will_restart_in_s_seconds": "Сервер будзе перазагружаны у %s секунд", diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index ac7c8ddcd000..f7bc5ad2cf8f 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -2363,8 +2363,7 @@ "Thank_you_for_your_feedback": "Благодарим Ви за обратната връзка", "The_application_name_is_required": "Името на приложението е задължително", "The_channel_name_is_required": "Името на канала е задължително", - "The_emails_are_being_sent": "Изпращат се имейлите.", - "The_field_is_required": "Полето %s е задължително.", + "The_emails_are_being_sent": "Изпращат се имейлите.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Размерът на изображението няма да работи, защото не можем да открием ImageMagick или GraphicsMagick, инсталирани на вашия сървър.", "The_redirectUri_is_required": "Необходимо е пренасочването", "The_server_will_restart_in_s_seconds": "Сървъра ще бъде рестартиран след %s секунди", diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index ebe0b045c418..85e70bbb0abc 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -2360,8 +2360,7 @@ "Thank_you_for_your_feedback": "Hvala vam na povratnim informacijama", "The_application_name_is_required": "Naziv aplikacije je potreban", "The_channel_name_is_required": "Ime sobe je potrebno", - "The_emails_are_being_sent": "E-mailovi su poslani.", - "The_field_is_required": "Polje %s je traženo.", + "The_emails_are_being_sent": "E-mailovi su poslani.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Promjena veličine slike neće raditi, jer ne možemo provjeriti da je ImageMagick ili GraphicsMagick instaliran na vašem poslužitelju.", "The_redirectUri_is_required": "RedirectUri je potrebno", "The_server_will_restart_in_s_seconds": "Poslužitelj će se ponovno pokrenuti u %s sekundi", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 3f74fc967416..f4d304e08633 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -4044,8 +4044,7 @@ "The_application_name_is_required": "El nom de laplicació és obligatori.", "The_channel_name_is_required": "Es requereix el nom del canal", "The_emails_are_being_sent": "Els missatges de correu-e s'estan enviant.", - "The_empty_room__roomName__will_be_removed_automatically": "La sala buida {{roomName}} s'eliminarà automàticament.", - "The_field_is_required": "El camp %s és obligatori.", + "The_empty_room__roomName__will_be_removed_automatically": "La sala buida {{roomName}} s'eliminarà automàticament.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "L'ajust de mida de les imatges no funcionarà perquè no podem detectar ni ImageMagick ni GraphicsMagick al servidor.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "El missatge és una discussió, no podrà recuperar els missatges!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Les notificacions mòbils es deshabilitaron per a tots els usuaris, aneu a \"Admin> Push\" per habilitar Push inici novament", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 914d6c35d485..a74224912766 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -3413,8 +3413,7 @@ "The_application_name_is_required": "Název aplikace je vyžadován", "The_channel_name_is_required": "Název místnosti je vyžadován", "The_emails_are_being_sent": "Tyto e-maily jsou odesílány.", - "The_empty_room__roomName__will_be_removed_automatically": "Prázdná místnost {{roomName}} bude automaticky odstraněna.", - "The_field_is_required": "Pole %s je povinné.", + "The_empty_room__roomName__will_be_removed_automatically": "Prázdná místnost {{roomName}} bude automaticky odstraněna.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Nelze změnit velikost obrázku, protože na serveru nebyl nalezen ImageMagick ani GraphicsMagick.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Zpráva je diskuse, ze které nebudete moci obnovit zprávy!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Mobilní oznámení byla deaktivována všem uživatelům, v admin sekci \"Notifikace\" znovu aktivujte Push Gateway", diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index f9c7b4d7ec87..7f63d511201f 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -2361,8 +2361,7 @@ "Thank_you_for_your_feedback": "Diolch i chi am eich adborth", "The_application_name_is_required": "Mae angen enw'r cais", "The_channel_name_is_required": "Mae angen enw'r sianel", - "The_emails_are_being_sent": "Mae'r negeseuon e-bost yn cael eu hanfon.", - "The_field_is_required": "Mae'r maes %s yn ofynnol.", + "The_emails_are_being_sent": "Mae'r negeseuon e-bost yn cael eu hanfon.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Ni fydd maint y delwedd yn gweithio oherwydd na allwn ganfod ImageMagick neu GraphicsMagick ar eich gweinydd.", "The_redirectUri_is_required": "Mae angen y redirectUri", "The_server_will_restart_in_s_seconds": "Bydd y gweinydd yn ailgychwyn yn %s eiliad", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index a675ab4843c0..2aabbfcd8642 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -3521,8 +3521,7 @@ "The_application_name_is_required": "Ansøgningsnavnet er påkrævet", "The_channel_name_is_required": "Kanalnavnet er påkrævet", "The_emails_are_being_sent": "E-mailsne bliver sendt.", - "The_empty_room__roomName__will_be_removed_automatically": "Det tomme rum {{roomName}} fjernes automatisk.", - "The_field_is_required": "Feltet%s er påkrævet.", + "The_empty_room__roomName__will_be_removed_automatically": "Det tomme rum {{roomName}} fjernes automatisk.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Billedforstørrelsen fungerer ikke, fordi vi ikke kan opdage ImageMagick eller GraphicsMagick installeret på din server.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Meddelelsen er en diskussion og du vil derfor ikke kunne gendanne meddelelserne!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Mobilmeddelelser blev deaktiveret for alle brugere. Tilgå \"Admin > Push\" for at aktivere Push Gateway igen", diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index 0be8030f5738..bbe960596a28 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -2368,8 +2368,7 @@ "Thank_you_for_your_feedback": "Vielen Dank für Ihre Rückmeldung.", "The_application_name_is_required": "Es muss ein Name für diese Anwendung angegeben werden.", "The_channel_name_is_required": "Ein Name für den Raum muss angegeben werden.", - "The_emails_are_being_sent": "Die E-Mails werden gesendet.", - "The_field_is_required": "Das Feld %s ist erforderlich.", + "The_emails_are_being_sent": "Die E-Mails werden gesendet.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Die automatische Skalierung der Bilder funktioniert nicht, da ImageMagick oder GraphicsMagick nicht auf dem Server installiert sind.", "The_redirectUri_is_required": "Es muss eine Weiterleitung-URL angegeben werden.", "The_server_will_restart_in_s_seconds": "Der Server wird in %s Sekunden neu gestartet", diff --git a/packages/i18n/src/locales/de-IN.i18n.json b/packages/i18n/src/locales/de-IN.i18n.json index 41da8fb3c809..edccd128e314 100644 --- a/packages/i18n/src/locales/de-IN.i18n.json +++ b/packages/i18n/src/locales/de-IN.i18n.json @@ -2661,8 +2661,7 @@ "Thank_you_for_your_feedback": "Vielen Dank für Deine Rückmeldung", "The_application_name_is_required": "Es muss ein Name für diese Anwendung angegeben werden", "The_channel_name_is_required": "Ein Name für den Kanal muss angegeben werden", - "The_emails_are_being_sent": "E-Mails werden gesendet", - "The_field_is_required": "Das Feld %s ist erforderlich", + "The_emails_are_being_sent": "E-Mails werden gesendet", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Die automatische Skalierung der Bilder funktioniert nicht, da ImageMagick oder GraphicsMagick nicht auf dem Server installiert sind.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Diese Nachricht ist eine Diskussion. Wenn Du diese löschst, wirst Du die Nachrichten der Diskussion nicht mehr auffinden können.", "The_peer__peer__does_not_exist": "Der Peer {{peer}} ist nicht vorhanden.", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 72963c308e67..59e329014186 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -4635,8 +4635,7 @@ "The_application_name_is_required": "Es muss ein Name für diese Anwendung angegeben werden", "The_channel_name_is_required": "Ein Name für den Channel muss angegeben werden", "The_emails_are_being_sent": "E-Mails werden gesendet", - "The_empty_room__roomName__will_be_removed_automatically": "Der leere Raum {{roomName}} wird automatisch entfernt.", - "The_field_is_required": "Das Feld %s ist erforderlich", + "The_empty_room__roomName__will_be_removed_automatically": "Der leere Raum {{roomName}} wird automatisch entfernt.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Die automatische Skalierung der Bilder funktioniert nicht, da ImageMagick oder GraphicsMagick nicht auf dem Server installiert sind.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Diese Nachricht ist eine Diskussion. Wenn Sie diese löschen, werden Sie die Nachrichten der Diskussion nicht mehr auffinden können.", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Die mobilen Benachrichtigungen wurden für alle Benutzer deaktiviert, wechseln Sie zu \"Admin > Push\", um das Push-Gateway erneut zu aktivieren", diff --git a/packages/i18n/src/locales/el.i18n.json b/packages/i18n/src/locales/el.i18n.json index 731a56695f7a..3e335c6638d3 100644 --- a/packages/i18n/src/locales/el.i18n.json +++ b/packages/i18n/src/locales/el.i18n.json @@ -2373,8 +2373,7 @@ "Thank_you_for_your_feedback": "Ευχαριστούμε για τα σχόλιά σας", "The_application_name_is_required": "Το όνομα της εφαρμογής απαιτείται", "The_channel_name_is_required": "Το όνομα του καναλιού απαιτείται", - "The_emails_are_being_sent": "Τα μηνύματα ηλεκτρονικού ταχυδρομείου που αποστέλλονται.", - "The_field_is_required": "Το πεδίο %s απαιτείται.", + "The_emails_are_being_sent": "Τα μηνύματα ηλεκτρονικού ταχυδρομείου που αποστέλλονται.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Το μέγεθος της εικόνας δεν θα λειτουργήσει, επειδή δεν μπορεί να ανιχνεύσει ImageMagick ή GraphicsMagick εγκατεστημένο στον server σας.", "The_redirectUri_is_required": "Η redirectUri απαιτείται", "The_server_will_restart_in_s_seconds": "Ο διακομιστής θα κάνει επανεκκίνηση στο %s δευτερόλεπτα", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 849582f934b7..796e9d0519ff 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1682,7 +1682,7 @@ "Direct_Message": "Direct message", "Livechat_Facebook_Enabled": "Facebook integration enabled", "Direct_message_creation_description": "Select one or more people to message", - "Direct_message_creation_error": "Please select at least one person", + "Direct_message_creation_error": "Select at least one person", "Direct_message_creation_description_hint": "More people cannot be added once created", "Direct_message_someone": "Direct message someone", "Direct_message_you_have_joined": "You have joined a new direct message with", @@ -4239,7 +4239,6 @@ "Please_fill_all_the_information": "Please fill all the information", "Please_fill_an_email": "Please fill an email", "Please_fill_name_and_email": "Please fill name and email", - "Please_fill_out_reason_for_report": "Please fill out the reason for the report", "Please_select_an_user": "Please select an user", "Please_select_enabled_yes_or_no": "Please select an option for Enabled", "Please_select_visibility": "Please select a visibility", @@ -4390,6 +4389,7 @@ "Real_Time_Monitoring": "Real-time Monitoring", "RealName_Change_Disabled": "Your Rocket.Chat administrator has disabled the changing of names", "Reason_for_joining": "Reason for joining", + "Reason_for_report": "Reason for report", "Reason_To_Join": "Reason to Join", "Receive_alerts": "Receive alerts", "Receive_Group_Mentions": "Receive @all and @here mentions", @@ -4583,6 +4583,7 @@ "Robot_Instructions_File_Content": "Robots.txt File Contents", "Root": "Root", "Required_action": "Required action", + "Required_field": "{{field}} required", "Default_Referrer_Policy": "Default Referrer Policy", "Default_Referrer_Policy_Description": "This controls the 'referrer' header that's sent when requesting embedded media from other servers. For more information, refer to [this link from MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy). Remember, a full page refresh is required for this to take effect", "No_feature_to_preview": "No feature to preview", @@ -5279,7 +5280,6 @@ "The_channel_name_is_required": "The channel name is required", "The_emails_are_being_sent": "The emails are being sent.", "The_empty_room__roomName__will_be_removed_automatically": "The empty room {{roomName}} will be removed automatically.", - "The_field_is_required": "The field %s is required.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "The image resize will not work because we can not detect ImageMagick or GraphicsMagick installed on your server.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "The message is a discussion you will not be able to recover the messages!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "The mobile notifications were disabled to all users, go to \"Admin > Push\" to enable the Push Gateway again", diff --git a/packages/i18n/src/locales/eo.i18n.json b/packages/i18n/src/locales/eo.i18n.json index 1558424c9445..907767a7f7da 100644 --- a/packages/i18n/src/locales/eo.i18n.json +++ b/packages/i18n/src/locales/eo.i18n.json @@ -2366,8 +2366,7 @@ "Thank_you_for_your_feedback": "Dankon pro viaj sugestoj", "The_application_name_is_required": "La aplika nomo estas postulita", "The_channel_name_is_required": "La kanala nomo estas postulita", - "The_emails_are_being_sent": "La retpoŝtoj estas senditaj.", - "The_field_is_required": "La kampo%s estas postulita.", + "The_emails_are_being_sent": "La retpoŝtoj estas senditaj.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "La regrandigo de bildoj ne funkcios ĉar ni ne povas detekti ImageMagick aŭ GraphicsMagick instalitan sur via servilo.", "The_redirectUri_is_required": "La redirectUri estas postulita", "The_server_will_restart_in_s_seconds": "La servilo rekomencos en%s sekundoj", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index a276a3c51690..4a690dd7a4b2 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -4109,8 +4109,7 @@ "The_application_name_is_required": "El nombre de la aplicación es obligatorio", "The_channel_name_is_required": "El nombre del canal es obligatorio", "The_emails_are_being_sent": "Los correos electrónicos se están enviando.", - "The_empty_room__roomName__will_be_removed_automatically": "La sala vacía {{roomName}} se eliminará automáticamente.", - "The_field_is_required": "El campo %s es obligatorio.", + "The_empty_room__roomName__will_be_removed_automatically": "La sala vacía {{roomName}} se eliminará automáticamente.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "El ajuste de tamaño de las imágenes no funcionará porque no detectamos ImageMagick o GraphicsMagick instalados en tu servidor.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "El mensaje es una discusión, así que no podrás recuperar los mensajes", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Las notificaciones móviles se han deshabilitado para todos los usuarios. Ve a \"Administración\" > \"Push\" para habilitar puerta de enlace push nuevamente", diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index c4850a7d30eb..b14bc31d7733 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -2702,7 +2702,6 @@ "The_application_name_is_required": "نام نرم افزار مورد نیاز است", "The_channel_name_is_required": "نام کانال نیاز است", "The_emails_are_being_sent": "ایمیل در حال ارسال.", - "The_field_is_required": "زمینه به %s مورد نیاز است.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "تغییر اندازه تصویر به کار نخواهد کرد زیرا ما نمی توانیم تشخیص ImageMagick را یا GraphicsMagick بر روی سرور خود نصب شده است.", "The_redirectUri_is_required": "redirectUri مورد نیاز است", "The_server_will_restart_in_s_seconds": "سرور در %s ثانیه راه اندازی مجدد خواهد", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index 17b5f65eab31..26ba8019aeb1 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -4727,8 +4727,7 @@ "The_application_will_be_able_to": "<1>{{appName}} voi:", "The_channel_name_is_required": "Kanavan nimi on pakollinen", "The_emails_are_being_sent": "Sähköpostiviestejä lähetetään.", - "The_empty_room__roomName__will_be_removed_automatically": "Tyhjä huone {{roomName}} poistetaan automaattisesti.", - "The_field_is_required": "Kenttä %s on pakollinen.", + "The_empty_room__roomName__will_be_removed_automatically": "Tyhjä huone {{roomName}} poistetaan automaattisesti.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Kuvien koon muuttaminen ei toimi, koska ImageMagickia tai GraphicsMagickia ei havaittu asennettuna palvelimessa.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Viesti on keskustelu, et voi palauttaa näitä viestejä!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Mobiili-ilmoitukset oli poistettu käytöstä kaikilta käyttäjiltä. Ota Push Gateway uudelleen käyttöön kohdassa \"Admin > Push\"", diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index 84d33eaf5330..80e4504d2085 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -4121,8 +4121,7 @@ "The_application_name_is_required": "Le nom de l'application est requis", "The_channel_name_is_required": "Le nom du canal est obligatoire", "The_emails_are_being_sent": "Les e-mails sont en cours d'envoi.", - "The_empty_room__roomName__will_be_removed_automatically": "Le salon vide {{roomName}} sera supprimé automatiquement.", - "The_field_is_required": "Le champ %s est requis.", + "The_empty_room__roomName__will_be_removed_automatically": "Le salon vide {{roomName}} sera supprimé automatiquement.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Le redimensionnement de l'image ne fonctionnera pas car nous n'avons pas trouvé ImageMagick ou GraphicsMagick installé sur votre serveur.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Le message est une discussion, vous ne pourrez pas récupérer les messages.", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Les notifications mobiles ont été désactivées pour tous les utilisateurs, accédez à \"Admin > Push\" pour réactiver la passerelle Push", diff --git a/packages/i18n/src/locales/he.i18n.json b/packages/i18n/src/locales/he.i18n.json index 33a4b920b4b2..499e2ca41d2d 100644 --- a/packages/i18n/src/locales/he.i18n.json +++ b/packages/i18n/src/locales/he.i18n.json @@ -1304,8 +1304,7 @@ "Thank_you_for_your_feedback": "תודה לך על המשוב", "The_application_name_is_required": "שם האפליקציה נדרש", "The_channel_name_is_required": "שם הערוץ נדרש", - "The_emails_are_being_sent": "האימיילים נשלחים.", - "The_field_is_required": "השדה %s הוא חובה.", + "The_emails_are_being_sent": "האימיילים נשלחים.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "שינוי גודל התמונה לא יעבוד כי אנחנו לא יכולים לזהות ImageMagick או GraphicsMagick מותקן על השרת שלך.", "The_redirectUri_is_required": "RedirectUri נדרש", "The_server_will_restart_in_s_seconds": "השרת יפעיל את עצמו מחדש בעוד ", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 1049d8495d86..090e081e83fa 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -4010,7 +4010,6 @@ "Please_fill_all_the_information": "कृपया सारी जानकारी भरें", "Please_fill_an_email": "कृपया एक ईमेल भरें", "Please_fill_name_and_email": "कृपया नाम और ईमेल भरें", - "Please_fill_out_reason_for_report": "कृपया रिपोर्ट का कारण भरें", "Please_select_an_user": "कृपया एक उपयोगकर्ता चुनें", "Please_select_enabled_yes_or_no": "कृपया सक्षम के लिए एक विकल्प चुनें", "Please_select_visibility": "कृपया एक दृश्यता चुनें", @@ -4988,8 +4987,7 @@ "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", "The_channel_name_is_required": "चैनल का नाम आवश्यक है", "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", - "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", - "The_field_is_required": "फ़ील्ड %s आवश्यक है.", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index 84c4295d5201..c9547b1042e9 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -2501,7 +2501,6 @@ "The_application_name_is_required": "Naziv aplikacije je potreban", "The_channel_name_is_required": "Ime sobe je potrebno", "The_emails_are_being_sent": "E-mailovi su poslani.", - "The_field_is_required": "Polje %s je traženo.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Promjena veličine slike neće raditi, jer ne možemo provjeriti da je ImageMagick ili GraphicsMagick instaliran na vašem poslužitelju.", "The_redirectUri_is_required": "RedirectUri je potrebno", "The_server_will_restart_in_s_seconds": "Poslužitelj će se ponovno pokrenuti u %s sekundi", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index f7f85536c183..a281e3b2c64b 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -4540,8 +4540,7 @@ "The_application_name_is_required": "Az alkalmazás neve kötelező", "The_channel_name_is_required": "A csatorna neve kötelező", "The_emails_are_being_sent": "Az e-mailek elküldésre kerültek.", - "The_empty_room__roomName__will_be_removed_automatically": "Az üres {{roomName}} szoba automatikusan el lesz távolítva.", - "The_field_is_required": "A(z) %s mező kötelező.", + "The_empty_room__roomName__will_be_removed_automatically": "Az üres {{roomName}} szoba automatikusan el lesz távolítva.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "A kép átméretezése nem fog működni, mert nem észlelhető a kiszolgálóra telepített ImageMagick vagy GraphicsMagick program.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Az üzenet egy megbeszélés, nem lesz képes helyreállítani az üzeneteket!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "A mobil értesítések le lettek tiltva az összes felhasználó számára. Menjen az „Adminisztráció → Leküldés” menüponthoz a leküldéses átjáró újbóli engedélyezéséhez.", diff --git a/packages/i18n/src/locales/id.i18n.json b/packages/i18n/src/locales/id.i18n.json index 1abc2fadd68c..6eb8dd75ec6e 100644 --- a/packages/i18n/src/locales/id.i18n.json +++ b/packages/i18n/src/locales/id.i18n.json @@ -2374,7 +2374,6 @@ "The_application_name_is_required": "Nama aplikasi diperlukan", "The_channel_name_is_required": "Nama channel diperlukan", "The_emails_are_being_sent": "Email telah dikirim.", - "The_field_is_required": "Kolom %s wajib diisi", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Gambar resize tidak akan bekerja karena kita tidak dapat mendeteksi ImageMagick atau GraphicsMagick diinstal pada server Anda.", "The_redirectUri_is_required": "The redirectUri diperlukan", "The_server_will_restart_in_s_seconds": "server akan restart%s detik", diff --git a/packages/i18n/src/locales/it.i18n.json b/packages/i18n/src/locales/it.i18n.json index 160dbe3cf40e..f4bbf05e2faf 100644 --- a/packages/i18n/src/locales/it.i18n.json +++ b/packages/i18n/src/locales/it.i18n.json @@ -2920,8 +2920,7 @@ "Thank_you_for_your_feedback": "Grazie per il tuo feedback", "The_application_name_is_required": "Il nome dell'applicazione è richiesta", "The_channel_name_is_required": "Il nome del canale è richiesto", - "The_emails_are_being_sent": "Le email verranno inviate.", - "The_field_is_required": "Il campo %s è richiesto.", + "The_emails_are_being_sent": "Le email verranno inviate.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Il ridimensionamento dell'immagine non funzionerà se non rileva ImageMagick or GraphicsMagick installato sul tuo server.", "The_redirectUri_is_required": "Il redirectUri é richiesto", "The_server_will_restart_in_s_seconds": "Il server si riavvierà in %s secondi", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index 982f9495d914..dcdf00bf7ed6 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -4070,8 +4070,7 @@ "The_application_name_is_required": "アプリケーション名は必須です", "The_channel_name_is_required": "チャネル名は必須です", "The_emails_are_being_sent": "メールを送信中です。", - "The_empty_room__roomName__will_be_removed_automatically": "空のルーム{{roomName}}は自動的に削除されます。", - "The_field_is_required": "%s 項目は、必須です。", + "The_empty_room__roomName__will_be_removed_automatically": "空のルーム{{roomName}}は自動的に削除されます。", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "サーバーにインストールされたImageMagickまたはGraphicsMagickを検出できないため、画像のサイズ変更が機能しません。", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "このメッセージはメッセージを復元できないディスカッションです!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "すべてのユーザーに対してモバイル通知が無効になりました。[管理]>[プッシュ]に移動して、プッシュゲートウェイを再度有効にしてください", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index 3c550632cc4c..8f2098ff495f 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -3165,7 +3165,6 @@ "The_channel_name_is_required": "საჭიროა არხის სახელი", "The_emails_are_being_sent": "ელ.ფოსტა იგზავნება.", "The_empty_room__roomName__will_be_removed_automatically": "ცარიელი ოთახი {{roomName}} ავტომატურად მოიხსნება.", - "The_field_is_required": "ველი %s აუცილებელია.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "სურათის ზომის შეცვლა არ იმუშავებს, რადგან ჩვენ ვერ ვპოულობთ თქვენს სერვერზე დაყენებულ ImageMagick ან GraphicsMagick-ს.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "შეტყობინება არის განხილვა და თქვენ ვერ შეძლებთ შეტყობინებების აღდგენას", "The_redirectUri_is_required": "გადამისამართების ლინკია საჭირო", diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index 997ee66a00e7..8927c7f3335b 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -2691,8 +2691,7 @@ "Thank_you_for_your_feedback": "សូមអរគុណសម្រាប់មតិរបស់អ្នក", "The_application_name_is_required": "ឈ្មោះកម្មវិធីនេះគឺត្រូវបានទាមទារ", "The_channel_name_is_required": "ឈ្មោះឆានែលនេះគឺត្រូវបានទាមទារ", - "The_emails_are_being_sent": "នេះ​អ៊ីមែល​ត្រូវ​បាន​បញ្ជូន​។", - "The_field_is_required": "ចន្លោះ %s ត្រូវ​បំពេញ", + "The_emails_are_being_sent": "នេះ​អ៊ីមែល​ត្រូវ​បាន​បញ្ជូន​។", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "ប្ដូរទំហំរូបភាពនឹងមិនធ្វើការដោយសារតែយើងមិនអាចរកឃើញ ImageMagick ឬ GraphicsMagick ដែលបានដំឡើងនៅលើម៉ាស៊ីនបម្រើរបស់អ្នក។", "The_redirectUri_is_required": "redirectUri នេះគឺត្រូវបានទាមទារ", "The_server_will_restart_in_s_seconds": "ម៉ាស៊ីនបម្រើនេះនឹងចាប់ផ្ដើមឡើងវិញនៅក្នុង %s វិនាទី", diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index ad5818ab9f85..5df1f0db78ba 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -3470,8 +3470,7 @@ "The_application_name_is_required": "응용프로그램명이 필요합니다.", "The_channel_name_is_required": "채널명이 필요합니다.", "The_emails_are_being_sent": "이메일을 전송 중입니다.", - "The_empty_room__roomName__will_be_removed_automatically": "사용하지 않는 대화방 {{roomName}}은 자동으로 제거됩니다.", - "The_field_is_required": "%s 필드는 필수 항목입니다.", + "The_empty_room__roomName__will_be_removed_automatically": "사용하지 않는 대화방 {{roomName}}은 자동으로 제거됩니다.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "서버에 ImageMagick 또는 GraphicMagick이 설치되어 있지 않아 이미지 크기를 재조정할 수 없습니다.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "그 메시지는 메시지 복구가 불가능한 토론입니다.", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "모바일 알림이 모든 사용자에게 사용 중지되었습니다. \"관리자> 푸시\"로 이동하여 푸시 게이트웨이를 다시 사용으로 변경하십시오.", diff --git a/packages/i18n/src/locales/ku.i18n.json b/packages/i18n/src/locales/ku.i18n.json index 9b4a4d1aaeb2..a4c5e7108a2f 100644 --- a/packages/i18n/src/locales/ku.i18n.json +++ b/packages/i18n/src/locales/ku.i18n.json @@ -2360,8 +2360,7 @@ "Thank_you_for_your_feedback": "Spas ji bo we Deng xwe", "The_application_name_is_required": "Navê sepanê pêwîst e", "The_channel_name_is_required": "The name kanala pêwîst e", - "The_emails_are_being_sent": "Mailan têne şandin.", - "The_field_is_required": "Li qadê %s tê xwestin.", + "The_emails_are_being_sent": "Mailan têne şandin.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Mezinahiyê image xebata wê ne ji ber ku em nikarin bi durustî .Pêşîn an GraphicsMagick li ser server te sazkirin.", "The_redirectUri_is_required": "The redirectUri pêwîst e", "The_server_will_restart_in_s_seconds": "ڕاژەکارەکە هەڵ ئەبێتەوە لە %s چرکەدا", diff --git a/packages/i18n/src/locales/lo.i18n.json b/packages/i18n/src/locales/lo.i18n.json index d427b251e863..547e9274a6ff 100644 --- a/packages/i18n/src/locales/lo.i18n.json +++ b/packages/i18n/src/locales/lo.i18n.json @@ -2403,8 +2403,7 @@ "Thank_you_for_your_feedback": "ຂໍ​ຂອບ​ໃຈ​ທ່ານ​ສໍາ​ລັບ​ການ​ຕໍາ​ນິ​ຕິ​ຊົມ​ຂອງ​ທ່ານ​", "The_application_name_is_required": "ຊື່ຄໍາຮ້ອງສະຫມັກແມ່ນຈໍາເປັນຕ້ອງ", "The_channel_name_is_required": "ຊື່ຊ່ອງທາງການຈໍາເປັນຕ້ອງ", - "The_emails_are_being_sent": "ອີເມວໄດ້ຖືກສົ່ງໄປແລ້ວ.", - "The_field_is_required": "ພາກສະຫນາມ %s ແມ່ນຕ້ອງການ.", + "The_emails_are_being_sent": "ອີເມວໄດ້ຖືກສົ່ງໄປແລ້ວ.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "ການ resize ຮູບພາບຈະບໍ່ເຮັດວຽກເພາະວ່າພວກເຮົາບໍ່ສາມາດກວດພົບ ImageMagick ຫຼື GraphicsMagick ການຕິດຕັ້ງໃນເຄື່ອງແມ່ຂ່າຍຂອງທ່ານ.", "The_redirectUri_is_required": "The redirectUri ຈໍາເປັນຕ້ອງມີ", "The_server_will_restart_in_s_seconds": "ເຄື່ອງແມ່ຂ່າຍຂອງຈະເລີ່ມການເຮັດວຽກໃນ %s ວິນາທີ", diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index 88fb66dc554f..7408a8eeb762 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -2421,8 +2421,7 @@ "Thank_you_for_your_feedback": "Dėkojame už jūsų atsiliepimus", "The_application_name_is_required": "Būtina nurodyti programos pavadinimą", "The_channel_name_is_required": "Reikalingas kanalo pavadinimas", - "The_emails_are_being_sent": "El. Laiškai siunčiami.", - "The_field_is_required": "Laukas%s reikalingas.", + "The_emails_are_being_sent": "El. Laiškai siunčiami.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Vaizdo dydžio keitimas neveiks, nes negalime nustatyti serverio \"ImageMagick\" ar \"GraphicsMagick\".", "The_redirectUri_is_required": "Reikia redirectUri", "The_server_will_restart_in_s_seconds": "Serveris bus paleistas iš naujo po%s sekundžių", diff --git a/packages/i18n/src/locales/lv.i18n.json b/packages/i18n/src/locales/lv.i18n.json index 544eac3dbeb3..a9e38267b01c 100644 --- a/packages/i18n/src/locales/lv.i18n.json +++ b/packages/i18n/src/locales/lv.i18n.json @@ -2375,8 +2375,7 @@ "Thank_you_for_your_feedback": "Paldies par jūsu atsauksmēm", "The_application_name_is_required": "Pieteikuma nosaukums ir nepieciešams", "The_channel_name_is_required": "Ir nepieciešams kanāla nosaukums", - "The_emails_are_being_sent": "E-pasta ziņojumi tiek nosūtīti.", - "The_field_is_required": "Nepieciešams %s lauks.", + "The_emails_are_being_sent": "E-pasta ziņojumi tiek nosūtīti.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Attēla izmēru maiņa nestrādās, jo mēs nevaram atrast jūsu serverī uzinstalētu ImageMagick vai GraphicsMagick.", "The_redirectUri_is_required": "Nepieciešams RedirectUri ", "The_server_will_restart_in_s_seconds": "Serveris restartēsies pēc %s sekundēm", diff --git a/packages/i18n/src/locales/mn.i18n.json b/packages/i18n/src/locales/mn.i18n.json index 60e18063b49f..407c86309db8 100644 --- a/packages/i18n/src/locales/mn.i18n.json +++ b/packages/i18n/src/locales/mn.i18n.json @@ -2361,8 +2361,7 @@ "Thank_you_for_your_feedback": "Таны санал хүсэлтэнд баярлалаа", "The_application_name_is_required": "Програмын нэр шаардлагатай", "The_channel_name_is_required": "Сувгийн нэр шаардлагатай", - "The_emails_are_being_sent": "Э-мэйл илгээж байна.", - "The_field_is_required": "%s талбар шаардлагатай.", + "The_emails_are_being_sent": "Э-мэйл илгээж байна.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "ImageMagick эсвэл GraphicsMagick-г сервер дээрээ суулгаж чаддаггүй болохоор зургийн хэмжээг өөрчлөх боломжгүй.", "The_redirectUri_is_required": "RedirectUri шаардлагатай", "The_server_will_restart_in_s_seconds": "Сервер%s секундын дараа дахин эхлүүлэх болно", diff --git a/packages/i18n/src/locales/ms-MY.i18n.json b/packages/i18n/src/locales/ms-MY.i18n.json index 524260560a3f..9e203c69baf3 100644 --- a/packages/i18n/src/locales/ms-MY.i18n.json +++ b/packages/i18n/src/locales/ms-MY.i18n.json @@ -2372,8 +2372,7 @@ "Thank_you_for_your_feedback": "Terima kasih atas maklum balas anda", "The_application_name_is_required": "Nama permohonan diperlukan", "The_channel_name_is_required": "Nama saluran diperlukan", - "The_emails_are_being_sent": "E-mel yang sedang dihantar.", - "The_field_is_required": "Medan ini %s diperlukan", + "The_emails_are_being_sent": "E-mel yang sedang dihantar.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "The saiz semula imej tidak akan berfungsi kerana kita tidak dapat mengesan ImageMagick atau GraphicsMagick dipasang pada pelayan anda.", "The_redirectUri_is_required": "The redirectUri diperlukan", "The_server_will_restart_in_s_seconds": "Server akan memulakan semula dalam %s saat", diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index bf7f7ffb780c..9796181ae1bb 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -4109,8 +4109,7 @@ "The_application_name_is_required": "De naam van de applicatie is vereist", "The_channel_name_is_required": "De naam van het kanaal is vereist", "The_emails_are_being_sent": "De e-mails worden verzonden.", - "The_empty_room__roomName__will_be_removed_automatically": "De lege kamer {{roomName}} wordt automatisch verwijderd.", - "The_field_is_required": "Het veld %s is vereist.", + "The_empty_room__roomName__will_be_removed_automatically": "De lege kamer {{roomName}} wordt automatisch verwijderd.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Het wijzigen van de grootte van de afbeelding zal niet werken omdat we ImageMagick of GraphicsMagick niet kunnen detecteren die op uw server zijn geïnstalleerd.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Het bericht is een discussie, u kunt de berichten niet herstellen!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "De mobiele meldingen werden voor alle gebruikers uitgeschakeld, ga naar \"Admin > Push\" om de Push Gateway weer in te schakelen", diff --git a/packages/i18n/src/locales/no.i18n.json b/packages/i18n/src/locales/no.i18n.json index 5bdbf2fb89cc..a89485a5f6c3 100644 --- a/packages/i18n/src/locales/no.i18n.json +++ b/packages/i18n/src/locales/no.i18n.json @@ -3962,8 +3962,7 @@ "The_application_name_is_required": "Programnavnet kreves", "The_application_will_be_able_to": "<1>{{appName}} vil kunne:", "The_channel_name_is_required": "Kanalnavnet er påkrevd", - "The_emails_are_being_sent": "E-postene blir sendt.", - "The_field_is_required": "Feltet%s er påkrevd.", + "The_emails_are_being_sent": "E-postene blir sendt.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Bildestørrelsen fungerer ikke fordi vi ikke kan oppdage ImageMagick eller GraphicsMagick installert på serveren din.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Meldingen er en diskusjon, du vil ikke kunne gjenopprette meldingene!", "The_necessary_browser_permissions_for_location_sharing_are_not_granted": "De nødvendige nettlesertillatelsene for posisjonsdeling ble ikke gitt", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index b7d60c54ee2c..e909624ad4a9 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -4543,8 +4543,7 @@ "The_application_name_is_required": "Wymagana jest nazwa aplikacji", "The_channel_name_is_required": "Wymagana jest nazwa pokoju", "The_emails_are_being_sent": "Wiadomości e-mail są wysyłane.", - "The_empty_room__roomName__will_be_removed_automatically": "Pusty pokój {{roomName}} zostanie automatycznie usunięty.", - "The_field_is_required": "Pole %s jest wymagane.", + "The_empty_room__roomName__will_be_removed_automatically": "Pusty pokój {{roomName}} zostanie automatycznie usunięty.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Zmiana rozmiaru obrazu nie będzie działać, ponieważ nie możemy wykryć zainstalowanego ImageMagick lub GraphicsMagick na serwerze.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Wiadomość jest dyskusją, której nie będziesz w stanie odzyskać!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Powiadomienia mobilne zostały wyłączone dla wszystkich użytkowników, idź do \"Admin>Push\" aby uruchomić ponownie bramę push", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index dc62318cb230..b2575a190671 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -4223,8 +4223,7 @@ "The_application_name_is_required": "O nome do aplicativo é obrigatório", "The_channel_name_is_required": "O nome do canal é obrigatório", "The_emails_are_being_sent": "Os e-mails estão sendo enviados.", - "The_empty_room__roomName__will_be_removed_automatically": "A sala vazia {{roomName}} será removida automaticamente.", - "The_field_is_required": "O campo %s é obrigatório.", + "The_empty_room__roomName__will_be_removed_automatically": "A sala vazia {{roomName}} será removida automaticamente.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "O redimensionamento da imagem não vai funcionar porque não conseguimos detectar se o ImageMagick ou GraphicsMagick está instalado no seu servidor.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "A mensagem é uma discussão, você não poderá recuperar as mensagens!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "As notificações móveis foram desativadas para todos os usuários, vá para \"Admin > Push\" para ativar o Push Gateway novamente", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 8f4acc81dbdd..84d840b92a53 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -2729,8 +2729,7 @@ "Thank_you_for_your_feedback": "Obrigado pelo sua opinião", "The_application_name_is_required": "O nome da aplicação é obrigatório", "The_channel_name_is_required": "O nome do canal é obrigatório", - "The_emails_are_being_sent": "Os emails estão a ser enviados.", - "The_field_is_required": "O campo %s é obrigatório.", + "The_emails_are_being_sent": "Os emails estão a ser enviados.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "O redimensionamento da imagem não vai funcionar porque não conseguimos detectar ImageMagick ou GraphicsMagick instalado no seu servidor.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "A mensagem é uma discussão, não poderá recuperar as mensagens!", "The_peer__peer__does_not_exist": "O par {{peer}} não existe.", diff --git a/packages/i18n/src/locales/ro.i18n.json b/packages/i18n/src/locales/ro.i18n.json index aefc4d07065f..7266b1883386 100644 --- a/packages/i18n/src/locales/ro.i18n.json +++ b/packages/i18n/src/locales/ro.i18n.json @@ -2364,8 +2364,7 @@ "Thank_you_for_your_feedback": "Vă mulțumim pentru feedback", "The_application_name_is_required": "Este nevoie de numele aplicației", "The_channel_name_is_required": "Este nevoie de numele canalului", - "The_emails_are_being_sent": "E-mail-urile sunt trimise.", - "The_field_is_required": "Este nevoie de câmpul %s.", + "The_emails_are_being_sent": "E-mail-urile sunt trimise.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Redimensionarea imaginii nu va funcționa, deoarece nu putem detecta ImageMagick sau GraphicsMagick instalat pe server.", "The_redirectUri_is_required": "URI de redirectare este necesar", "The_server_will_restart_in_s_seconds": "Serverul va reporni în %s secunde", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index 1829f7f2eb08..6edc8030513f 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -4294,8 +4294,7 @@ "The_application_name_is_required": "Требуется название приложения", "The_channel_name_is_required": "Требуется название канала", "The_emails_are_being_sent": "Письма отправляются.", - "The_empty_room__roomName__will_be_removed_automatically": "Пустой чат {{roomName}} будет удален автоматически.", - "The_field_is_required": "Поле %s обязательно.", + "The_empty_room__roomName__will_be_removed_automatically": "Пустой чат {{roomName}} будет удален автоматически.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Изменить размер изображения не получится, потому что мы не можем обнаружить установленные на вашем сервере ImageMagick или GraphicsMagick.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Сообщение обсуждения, вы не сможете восстановить сообщения!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Мобильные уведомления были отключены для всех пользователей, перейдите в \"Admin > Push\", чтобы снова включить Push Gateway", diff --git a/packages/i18n/src/locales/sk-SK.i18n.json b/packages/i18n/src/locales/sk-SK.i18n.json index f5bc7bfcbc58..d6cf085ad972 100644 --- a/packages/i18n/src/locales/sk-SK.i18n.json +++ b/packages/i18n/src/locales/sk-SK.i18n.json @@ -2375,8 +2375,7 @@ "Thank_you_for_your_feedback": "Ďakujeme vám za vašu spätnú väzbu", "The_application_name_is_required": "Názov aplikácie je povinný", "The_channel_name_is_required": "Názov kanála je povinný", - "The_emails_are_being_sent": "Posielajú sa e-maily.", - "The_field_is_required": "Je potrebné pole%s.", + "The_emails_are_being_sent": "Posielajú sa e-maily.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Zmena veľkosti obrázka nebude fungovať, pretože na vašom serveri nemôžeme nájsť aplikácie ImageMagick alebo GraphicsMagick.", "The_redirectUri_is_required": "Vyžaduje sa presmerovanie", "The_server_will_restart_in_s_seconds": "Server sa reštartuje v%s", diff --git a/packages/i18n/src/locales/sl-SI.i18n.json b/packages/i18n/src/locales/sl-SI.i18n.json index 668bb94dc2c5..64900b03860d 100644 --- a/packages/i18n/src/locales/sl-SI.i18n.json +++ b/packages/i18n/src/locales/sl-SI.i18n.json @@ -2355,8 +2355,7 @@ "Thank_you_for_your_feedback": "Hvala za vaše mnenje", "The_application_name_is_required": "Ime aplikacije je obvezno", "The_channel_name_is_required": "Ime kanala je obvezno", - "The_emails_are_being_sent": "Pošiljanje e-poštnih sporočil.", - "The_field_is_required": "Polje %s je obvezno.", + "The_emails_are_being_sent": "Pošiljanje e-poštnih sporočil.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Velikost slike ne bo delovala, ker ne moremo zaznati ImageMagick ali GraphicsMagick, nameščenega na vašem strežniku.", "The_redirectUri_is_required": "Preusmeritveni uri (redirectUri) je zahtevan", "The_server_will_restart_in_s_seconds": "Strežnik se bo znova zagnal v %s sekundah", diff --git a/packages/i18n/src/locales/sq.i18n.json b/packages/i18n/src/locales/sq.i18n.json index 312494da2e8d..c5fb06df9529 100644 --- a/packages/i18n/src/locales/sq.i18n.json +++ b/packages/i18n/src/locales/sq.i18n.json @@ -2365,8 +2365,7 @@ "Thank_you_for_your_feedback": "Faleminderit për komentin tuaj", "The_application_name_is_required": "Emri i aplikimit është e nevojshme", "The_channel_name_is_required": "Emri kanal është e nevojshme", - "The_emails_are_being_sent": "Email janë duke u dërguar.", - "The_field_is_required": "Fusha %s është e nevojshme.", + "The_emails_are_being_sent": "Email janë duke u dërguar.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Imazhi resize nuk do të funksionojë, sepse ne nuk mund të zbulojë ImageMagick ose GraphicsMagick instaluar në serverin tuaj.", "The_redirectUri_is_required": "RedirectUri është e nevojshme", "The_server_will_restart_in_s_seconds": "Serveri do të rifillojë në %s sekonda", diff --git a/packages/i18n/src/locales/sr.i18n.json b/packages/i18n/src/locales/sr.i18n.json index e26e8bd3862f..d60ff9ce80fe 100644 --- a/packages/i18n/src/locales/sr.i18n.json +++ b/packages/i18n/src/locales/sr.i18n.json @@ -2170,8 +2170,7 @@ "Thank_you_for_your_feedback": "Хвала на повратним информацијама", "The_application_name_is_required": "Име апликације је потребно", "The_channel_name_is_required": "Име канала је потребно", - "The_emails_are_being_sent": "Е-поруке се шаљу.", - "The_field_is_required": "је обавезно поље %s.", + "The_emails_are_being_sent": "Е-поруке се шаљу.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Ресизе слика неће радити, јер не можемо открити ИмагеМагицк или грапхицсмагицк инсталиран на вашем серверу.", "The_redirectUri_is_required": "РедирецтУри је потребна", "The_server_will_restart_in_s_seconds": "Сервер ће се поново покренути у %s секунди", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index 4e436fe6c538..4f091bb77fa5 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -4735,8 +4735,7 @@ "The_application_will_be_able_to": "<1>{{appName}} kommer att kunna:", "The_channel_name_is_required": "Kanalnamnet krävs ", "The_emails_are_being_sent": "E-postmeddelandena skickas.", - "The_empty_room__roomName__will_be_removed_automatically": "Det tomma rummet {{roomName}} tas bort automatiskt.", - "The_field_is_required": "Fältet %s krävs.", + "The_empty_room__roomName__will_be_removed_automatically": "Det tomma rummet {{roomName}} tas bort automatiskt.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Det kommer inte att gå att ändra storlek på bilden, eftersom vi inte kan hitta ImageMagick eller GraphicsMagick på din server.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Meddelandet är en diskussion. Du kan inte återskapa meddelandena.", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "Mobilaviseringar inaktiverades för alla användare. Gå till \"Administration > Push\" om du vill aktivera en gateway för pushmeddelanden igen", diff --git a/packages/i18n/src/locales/ta-IN.i18n.json b/packages/i18n/src/locales/ta-IN.i18n.json index 7a2c562d4fff..c4c941c40d7b 100644 --- a/packages/i18n/src/locales/ta-IN.i18n.json +++ b/packages/i18n/src/locales/ta-IN.i18n.json @@ -2366,7 +2366,6 @@ "The_application_name_is_required": "விண்ணப்ப பெயர் தேவை", "The_channel_name_is_required": "சேனல் பெயர் தேவைப்படுகிறது", "The_emails_are_being_sent": "மின்னஞ்சல்களை அனுப்பி வைக்கப்படுகின்றனர்.", - "The_field_is_required": "துறையில்% கள் தேவை.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "நாங்கள் ImageMagick அல்லது GraphicsMagick உங்கள் சர்வரில் நிறுவப்பட்ட கண்டறிய முடியாது, ஏனெனில் படஅளவை இயங்காது.", "The_redirectUri_is_required": "redirecturi தேவை", "The_server_will_restart_in_s_seconds": "சர்வர் %s வினாடிகள் மீண்டும் தொடங்கும்", diff --git a/packages/i18n/src/locales/th-TH.i18n.json b/packages/i18n/src/locales/th-TH.i18n.json index 7d393a3034fb..520f6ce3a20f 100644 --- a/packages/i18n/src/locales/th-TH.i18n.json +++ b/packages/i18n/src/locales/th-TH.i18n.json @@ -2359,8 +2359,7 @@ "Thank_you_for_your_feedback": "ขอบคุณสำหรับความคิดเห็นของคุณ", "The_application_name_is_required": "ต้องระบุชื่อแอ็พพลิเคชัน", "The_channel_name_is_required": "ต้องระบุชื่อช่อง", - "The_emails_are_being_sent": "กำลังส่งอีเมล", - "The_field_is_required": "ต้องระบุฟิลด์%s", + "The_emails_are_being_sent": "กำลังส่งอีเมล", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "การปรับขนาดภาพจะไม่ทำงานเนื่องจากเราไม่สามารถตรวจพบ ImageMagick หรือ GraphicsMagick ที่ติดตั้งบนเซิร์ฟเวอร์ของคุณ", "The_redirectUri_is_required": "จำเป็นต้องมี redirectUri", "The_server_will_restart_in_s_seconds": "เซิร์ฟเวอร์จะรีสตาร์ทเป็น %s วินาที", diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 59b0c8927e23..c3e879031912 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -2801,8 +2801,7 @@ "Thank_you_for_your_feedback": "Görüşleriniz için teşekkür ederiz", "The_application_name_is_required": "Uygulama adı gerekli", "The_channel_name_is_required": "Kanal adı gerekli", - "The_emails_are_being_sent": "E-postalar gönderiliyor.", - "The_field_is_required": "%s alanı gerekli.", + "The_emails_are_being_sent": "E-postalar gönderiliyor.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Biz ImageMagick veya GraphicsMagick sunucunuzda yüklü algılayamaz çünkü resim boyutlandırmak çalışmaz.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Bu ileti bir tartışma olduğu için iletileri geri getiremeyeceksiniz!", "The_peer__peer__does_not_exist": "{{peer}} adında bir eş yok.", diff --git a/packages/i18n/src/locales/ug.i18n.json b/packages/i18n/src/locales/ug.i18n.json index 78c3650e0038..b576cd5102cd 100644 --- a/packages/i18n/src/locales/ug.i18n.json +++ b/packages/i18n/src/locales/ug.i18n.json @@ -1045,8 +1045,7 @@ "Thank_you_for_your_feedback": "سىزنىڭ ئىنكاسىڭىزگە كۆپتىن كۆپ رەھمەت", "The_application_name_is_required": "ئەپنىڭ ئىسمىنى چوقۇم تولدۇرىسىز", "The_channel_name_is_required": "قانالنىڭ ئىسمىنى چوقۇم تولدۇرۇڭ", - "The_emails_are_being_sent": "ئىلخەت يوللاندى", - "The_field_is_required": "چوقۇم تولدۇرۇڭ%sخەت بۆلىكى", + "The_emails_are_being_sent": "ئىلخەت يوللاندى", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "تاپالمىدۇق.GraphicsMagickنى ياكىImageMagick رەسىمنىڭ چوڭ كىچىكلىكىنى ئۆزگەرتكىلى بولمىدى، چۈنكى بىز مۇلازىمتېردا قاچىلىنىپ بولغان", "The_redirectUri_is_required": "چوقۇم تولدۇرۇڭURIسەكرەپ يۆتكەلگەن", "The_server_will_restart_in_s_seconds": "سېكۇنتتىن كېيىن قايتىدىن قوزغىتىلىدۇ %s مۇلازىمىتېر", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index b8e27a4f8c34..eb2b8542d4e3 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -2911,8 +2911,7 @@ "Thank_you_for_your_feedback": "Спасибі за ваш відгук", "The_application_name_is_required": "Ім'я програми потрібен", "The_channel_name_is_required": "Назва каналу потрібно", - "The_emails_are_being_sent": "Електронні листи відправляються.", - "The_field_is_required": "Поле %s потрібно.", + "The_emails_are_being_sent": "Електронні листи відправляються.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Зміни розміру зображення не буде працювати, тому що ми не можемо виявити ImageMagick або GraphicsMagick встановлений на вашому сервері.", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "Повідомлення - це обговорення яке ви не зможете відновити!", "The_redirectUri_is_required": "RedirectUri потрібно", diff --git a/packages/i18n/src/locales/vi-VN.i18n.json b/packages/i18n/src/locales/vi-VN.i18n.json index bf073e9c6aee..e4ff1343a079 100644 --- a/packages/i18n/src/locales/vi-VN.i18n.json +++ b/packages/i18n/src/locales/vi-VN.i18n.json @@ -2465,8 +2465,7 @@ "Thank_you_for_your_feedback": "Cảm ơn phản hồi của bạn", "The_application_name_is_required": "Tên ứng dụng là bắt buộc", "The_channel_name_is_required": "Tên kênh là bắt buộc", - "The_emails_are_being_sent": "Các email đang được gửi.", - "The_field_is_required": "Trường%s là bắt buộc.", + "The_emails_are_being_sent": "Các email đang được gửi.", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "Thay đổi kích thước hình ảnh sẽ không hoạt động vì chúng tôi không thể phát hiện ImageMagick hoặc GraphicsMagick được cài đặt trên máy chủ của bạn.", "The_redirectUri_is_required": "RedirectUri là bắt buộc", "The_server_will_restart_in_s_seconds": "Máy chủ sẽ khởi động lại trong%s giây", diff --git a/packages/i18n/src/locales/zh-HK.i18n.json b/packages/i18n/src/locales/zh-HK.i18n.json index e9821173dc0c..2da4905ae336 100644 --- a/packages/i18n/src/locales/zh-HK.i18n.json +++ b/packages/i18n/src/locales/zh-HK.i18n.json @@ -2392,8 +2392,7 @@ "Thank_you_for_your_feedback": "感谢您的反馈意见", "The_application_name_is_required": "应用程序名称是必需的", "The_channel_name_is_required": "频道名称是必需的", - "The_emails_are_being_sent": "电子邮件正在发送。", - "The_field_is_required": "字段 %s 必须填写。", + "The_emails_are_being_sent": "电子邮件正在发送。", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "图像大小调整不起作用,因为我们无法检测到服务器上安装的ImageMagick或GraphicsMagick。", "The_redirectUri_is_required": "redirectUri是必需的", "The_server_will_restart_in_s_seconds": "服务器将以%s秒为单位重新启动", diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 741132996be6..7b8dc7495a84 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -3896,8 +3896,7 @@ "The_application_name_is_required": "應用程式名稱是必需的", "The_channel_name_is_required": "頻道名稱是必需的", "The_emails_are_being_sent": "電子郵件傳送中", - "The_empty_room__roomName__will_be_removed_automatically": "空房間 {{roomName}} 將自動刪除。", - "The_field_is_required": "欄位 %s 必填", + "The_empty_room__roomName__will_be_removed_automatically": "空房間 {{roomName}} 將自動刪除。", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "圖片大小調整不會起作用,因為在我們偵測不到伺服器上安裝的 ImageMagick 或 GraphicsMagick 工具", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "此訊息是在論壇中您不可以復原訊息!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "所有使用者都停用了手機通知,請到“管理>推送”以再次啟用推送閘道", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 323da5954332..f3e8c2e0fc9e 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -3556,8 +3556,7 @@ "The_application_name_is_required": "应用名称必填", "The_channel_name_is_required": "频道名称为必填", "The_emails_are_being_sent": "邮件已发送。", - "The_empty_room__roomName__will_be_removed_automatically": "空房间 {{roomName}} 将被自动移除。", - "The_field_is_required": "字段 %s 必填", + "The_empty_room__roomName__will_be_removed_automatically": "空房间 {{roomName}} 将被自动移除。", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "无法调整图片尺寸,因为我们无法在服务器上找到已安装的 ImageMagick 或 GraphicsMagick 。", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "此消息是一个讨论你将无法恢复此消息!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "所有用户的移动端通知都已禁用。前往 “管理 > 推送” 再次启用推送网关", diff --git a/packages/ui-client/src/components/CustomFieldsForm.tsx b/packages/ui-client/src/components/CustomFieldsForm.tsx index dc464fcd23c2..98b9c3fdad79 100644 --- a/packages/ui-client/src/components/CustomFieldsForm.tsx +++ b/packages/ui-client/src/components/CustomFieldsForm.tsx @@ -52,7 +52,7 @@ const CustomField = ({ (error: RHFFieldError) => { switch (error?.type) { case 'required': - return t('The_field_is_required', label || name); + return t('Required_field', { field: label || name }); case 'minLength': return t('Min_length_is', props?.minLength); case 'maxLength': diff --git a/packages/web-ui-registration/src/LoginForm.tsx b/packages/web-ui-registration/src/LoginForm.tsx index d63886091fa0..6e28a443be63 100644 --- a/packages/web-ui-registration/src/LoginForm.tsx +++ b/packages/web-ui-registration/src/LoginForm.tsx @@ -162,7 +162,7 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute (!passwordIsValid ? t('Password_must_meet_the_complexity_requirements') : true), })} error={errors.password?.message} @@ -247,7 +247,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo (watch('password') === val ? true : t('registration.component.form.invalidConfirmPass')), })} @@ -275,7 +275,7 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo Date: Sat, 14 Sep 2024 01:01:59 -0300 Subject: [PATCH 12/57] chore(deps): Patch dependencies (#33285) --- .../moleculer-npm-0.14.34-440e26767d.patch | 13 + apps/meteor/ee/server/services/package.json | 18 +- apps/meteor/package.json | 162 +- apps/uikit-playground/package.json | 14 +- ee/apps/account-service/package.json | 12 +- ee/apps/authorization-service/package.json | 10 +- ee/apps/ddp-streamer/package.json | 16 +- ee/apps/omnichannel-transcript/package.json | 12 +- ee/apps/presence-service/package.json | 10 +- ee/apps/queue-worker/package.json | 12 +- ee/apps/stream-hub-service/package.json | 12 +- ee/packages/license/package.json | 6 +- ee/packages/omnichannel-services/package.json | 4 +- ee/packages/pdf-worker/package.json | 12 +- ee/packages/ui-theming/package.json | 6 +- package.json | 9 +- packages/agenda/package.json | 4 +- packages/api-client/package.json | 4 +- packages/core-services/package.json | 6 +- packages/ddp-client/package.json | 4 +- packages/eslint-config/package.json | 4 +- packages/fuselage-ui-kit/package.json | 12 +- packages/gazzodown/package.json | 16 +- packages/jest-presets/package.json | 8 +- packages/jwt/package.json | 2 +- packages/livechat/package.json | 12 +- packages/log-format/package.json | 2 +- packages/message-parser/package.json | 20 +- packages/mock-providers/package.json | 2 +- packages/model-typings/package.json | 2 +- packages/models/package.json | 2 +- packages/password-policies/package.json | 2 +- packages/patch-injection/package.json | 2 +- packages/peggy-loader/package.json | 4 +- packages/release-action/package.json | 2 +- packages/release-changelog/package.json | 2 +- packages/rest-typings/package.json | 2 +- packages/server-fetch/package.json | 2 +- packages/tools/package.json | 4 +- packages/ui-avatar/package.json | 7 +- packages/ui-client/package.json | 13 +- packages/ui-composer/package.json | 9 +- packages/ui-contexts/package.json | 10 +- packages/ui-kit/package.json | 16 +- packages/ui-video-conf/package.json | 7 +- packages/web-ui-registration/package.json | 8 +- yarn.lock | 2133 +++++++++++------ 47 files changed, 1631 insertions(+), 1020 deletions(-) create mode 100644 .yarn/patches/moleculer-npm-0.14.34-440e26767d.patch diff --git a/.yarn/patches/moleculer-npm-0.14.34-440e26767d.patch b/.yarn/patches/moleculer-npm-0.14.34-440e26767d.patch new file mode 100644 index 000000000000..005ab83cfb48 --- /dev/null +++ b/.yarn/patches/moleculer-npm-0.14.34-440e26767d.patch @@ -0,0 +1,13 @@ +diff --git a/index.d.ts b/index.d.ts +index f64eda7889619f2a1e7cbd5bb5e3533fc8aaa182..3d815a3b7aaf426235aa92422129079aa5826908 100644 +--- a/index.d.ts ++++ b/index.d.ts +@@ -725,7 +725,7 @@ declare namespace Moleculer { + this: T + ) => void | Promise; + +- interface ServiceSchema { ++ interface ServiceSchema> { + name: string; + version?: string | number; + settings?: S; diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 7c51605d4190..43659382eb67 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -30,7 +30,7 @@ "@rocket.chat/ui-kit": "workspace:~", "ajv": "^8.11.0", "bcrypt": "^5.0.1", - "body-parser": "^1.20.2", + "body-parser": "^1.20.3", "colorette": "^2.0.20", "cookie": "^0.5.0", "cookie-parser": "^1.4.6", @@ -40,7 +40,7 @@ "fibers": "^5.0.3", "jaeger-client": "^3.19.0", "mem": "^8.1.1", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "mongodb": "^4.17.2", "nats": "^2.6.1", "pino": "^8.15.0", @@ -51,17 +51,17 @@ }, "devDependencies": { "@rocket.chat/icons": "~0.38.0", - "@types/cookie": "^0.5.3", - "@types/cookie-parser": "^1.4.5", - "@types/ejson": "^2.2.1", - "@types/express": "^4.17.20", - "@types/fibers": "^3.1.3", + "@types/cookie": "^0.5.4", + "@types/cookie-parser": "^1.4.7", + "@types/ejson": "^2.2.2", + "@types/express": "^4.17.21", + "@types/fibers": "^3.1.4", "@types/node": "^14.18.63", - "@types/ws": "^8.5.8", + "@types/ws": "^8.5.12", "npm-run-all": "^4.1.5", "pino-pretty": "^7.6.1", "pm2": "^5.2.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "volta": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index df12721c5261..9ee7e48d1794 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -66,7 +66,7 @@ "devDependencies": { "@axe-core/playwright": "^4.7.3", "@babel/core": "~7.22.20", - "@babel/eslint-parser": "~7.23.3", + "@babel/eslint-parser": "~7.23.10", "@babel/plugin-proposal-nullish-coalescing-operator": "~7.18.6", "@babel/plugin-proposal-optional-chaining": "~7.21.0", "@babel/preset-env": "~7.22.20", @@ -87,76 +87,76 @@ "@storybook/react": "~6.5.16", "@storybook/testing-library": "0.0.13", "@tanstack/react-query-devtools": "^4.19.1", - "@testing-library/react": "~16.0.0", + "@testing-library/react": "~16.0.1", "@testing-library/user-event": "~14.5.2", - "@types/adm-zip": "^0.5.3", + "@types/adm-zip": "^0.5.5", "@types/archiver": "^5.3.4", - "@types/bad-words": "^3.0.2", - "@types/bcrypt": "^5.0.1", - "@types/body-parser": "^1.19.4", - "@types/busboy": "^1.5.2", - "@types/chai": "~4.3.16", - "@types/chai-as-promised": "^7.1.7", - "@types/chai-datetime": "0.0.38", - "@types/chai-dom": "1.11.2", - "@types/chai-spies": "~1.0.5", - "@types/codemirror": "^5.60.12", - "@types/cookie-parser": "^1.4.5", - "@types/cors": "^2.8.15", - "@types/cssom": "^0.4.2", + "@types/bad-words": "^3.0.3", + "@types/bcrypt": "^5.0.2", + "@types/body-parser": "^1.19.5", + "@types/busboy": "^1.5.4", + "@types/chai": "~4.3.19", + "@types/chai-as-promised": "^7.1.8", + "@types/chai-datetime": "0.0.39", + "@types/chai-dom": "1.11.3", + "@types/chai-spies": "~1.0.6", + "@types/codemirror": "^5.60.15", + "@types/cookie-parser": "^1.4.7", + "@types/cors": "^2.8.17", + "@types/cssom": "^0.4.3", "@types/dompurify": "^2.3.3", - "@types/ejson": "^2.2.1", - "@types/express": "^4.17.20", + "@types/ejson": "^2.2.2", + "@types/express": "^4.17.21", "@types/express-rate-limit": "^5.1.3", - "@types/fibers": "^3.1.3", - "@types/google-libphonenumber": "^7.4.29", - "@types/gravatar": "^1.8.5", + "@types/fibers": "^3.1.4", + "@types/google-libphonenumber": "^7.4.30", + "@types/gravatar": "^1.8.6", "@types/he": "^1.1.2", - "@types/i18next-sprintf-postprocessor": "^0.2.2", - "@types/imap": "^0.8.39", - "@types/jest": "~29.5.12", + "@types/i18next-sprintf-postprocessor": "^0.2.3", + "@types/imap": "^0.8.40", + "@types/jest": "~29.5.13", "@types/jsdom": "^16.2.15", - "@types/jsdom-global": "^3.0.6", - "@types/jsrsasign": "^10.5.11", - "@types/later": "^1.2.8", + "@types/jsdom-global": "^3.0.7", + "@types/jsrsasign": "^10.5.14", + "@types/later": "^1.2.9", "@types/ldapjs": "^2.2.5", - "@types/less": "~3.0.5", + "@types/less": "~3.0.6", "@types/lodash.clonedeep": "^4.5.9", - "@types/lodash.get": "^4.4.8", - "@types/mailparser": "^3.4.3", + "@types/lodash.get": "^4.4.9", + "@types/mailparser": "^3.4.4", "@types/marked": "^4.0.8", "@types/meteor": "^2.9.8", - "@types/meteor-collection-hooks": "^0.8.8", + "@types/meteor-collection-hooks": "^0.8.9", "@types/mkdirp": "^1.0.2", "@types/mocha": "github:whitecolor/mocha-types", "@types/moment-timezone": "^0.5.30", "@types/node": "^14.18.63", - "@types/node-gcm": "^1.0.3", - "@types/node-rsa": "^1.1.3", - "@types/nodemailer": "^6.4.13", - "@types/oauth2-server": "^3.0.15", - "@types/parseurl": "^1.3.2", - "@types/prometheus-gc-stats": "^0.6.3", - "@types/proxyquire": "^1.3.30", - "@types/psl": "^1.1.2", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", - "@types/rewire": "^2.5.29", + "@types/node-gcm": "^1.0.5", + "@types/node-rsa": "^1.1.4", + "@types/nodemailer": "^6.4.15", + "@types/oauth2-server": "^3.0.17", + "@types/parseurl": "^1.3.3", + "@types/prometheus-gc-stats": "^0.6.4", + "@types/proxyquire": "^1.3.31", + "@types/psl": "^1.1.3", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", + "@types/rewire": "^2.5.30", "@types/sanitize-html": "^2.9.3", "@types/semver": "^7.3.10", "@types/sharp": "^0.30.5", "@types/sinon": "^10.0.20", - "@types/strict-uri-encode": "^2.0.1", + "@types/strict-uri-encode": "^2.0.2", "@types/string-strip-html": "^5.0.1", - "@types/supertest": "^2.0.15", - "@types/supports-color": "~7.2.0", - "@types/textarea-caret": "^3.0.2", - "@types/ua-parser-js": "^0.7.38", - "@types/use-subscription": "^1.0.1", - "@types/use-sync-external-store": "^0.0.5", + "@types/supertest": "^2.0.16", + "@types/supports-color": "~7.2.1", + "@types/textarea-caret": "^3.0.3", + "@types/ua-parser-js": "^0.7.39", + "@types/use-subscription": "^1.0.2", + "@types/use-sync-external-store": "^0.0.6", "@types/uuid": "^8.3.4", - "@types/xml-crypto": "~1.4.4", - "@types/xml-encryption": "~1.2.3", + "@types/xml-crypto": "~1.4.6", + "@types/xml-encryption": "~1.2.4", "@typescript-eslint/eslint-plugin": "~5.60.1", "@typescript-eslint/parser": "~5.60.1", "autoprefixer": "^9.8.8", @@ -164,12 +164,12 @@ "babel-plugin-array-includes": "^2.0.3", "babel-plugin-istanbul": "^6.1.1", "chai": "^4.3.10", - "chai-as-promised": "^7.1.1", - "chai-datetime": "^1.8.0", + "chai-as-promised": "^7.1.2", + "chai-datetime": "^1.8.1", "chai-dom": "^1.11.0", "chai-spies": "~1.0.0", "cross-env": "^7.0.3", - "docker-compose": "^0.24.3", + "docker-compose": "^0.24.8", "emojione-assets": "^4.5.0", "eslint": "~8.45.0", "eslint-config-prettier": "~8.8.0", @@ -179,7 +179,7 @@ "eslint-plugin-playwright": "~0.15.3", "eslint-plugin-prettier": "~4.2.1", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-testing-library": "~6.2.2", "eslint-plugin-you-dont-need-lodash-underscore": "~6.12.0", "fast-glob": "^3.2.12", @@ -190,8 +190,8 @@ "nyc": "^15.1.0", "outdent": "~0.8.0", "pino-pretty": "^7.6.1", - "playwright-qase-reporter": "^1.2.1", - "postcss": "~8.4.31", + "playwright-qase-reporter": "^1.2.2", + "postcss": "~8.4.45", "postcss-custom-properties": "^11.0.0", "postcss-easy-import": "^3.0.0", "postcss-load-config": "^3.1.4", @@ -200,7 +200,7 @@ "postcss-url": "^10.1.3", "prettier": "~2.8.8", "proxyquire": "^2.1.3", - "react-docgen-typescript-plugin": "^1.0.5", + "react-docgen-typescript-plugin": "^1.0.8", "rewire": "^6.0.0", "sinon": "^14.0.2", "source-map": "^0.7.4", @@ -209,7 +209,7 @@ "supertest": "^6.2.3", "supports-color": "~7.2.0", "template-file": "^6.0.1", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "dependencies": { @@ -217,7 +217,7 @@ "@bugsnag/js": "~7.20.2", "@bugsnag/plugin-react": "~7.19.0", "@google-cloud/storage": "^6.11.0", - "@kaciras/deasync": "^1.0.3", + "@kaciras/deasync": "^1.0.4", "@nivo/bar": "0.84.0", "@nivo/core": "0.84.0", "@nivo/heatmap": "0.84.0", @@ -252,7 +252,7 @@ "@rocket.chat/icons": "~0.38.0", "@rocket.chat/instance-status": "workspace:^", "@rocket.chat/jwt": "workspace:^", - "@rocket.chat/layout": "~0.31.26", + "@rocket.chat/layout": "~0.31.27", "@rocket.chat/license": "workspace:^", "@rocket.chat/log-format": "workspace:^", "@rocket.chat/logger": "workspace:^", @@ -287,15 +287,15 @@ "@slack/bolt": "^3.14.0", "@slack/rtm-api": "^6.0.0", "@tanstack/react-query": "^4.16.1", - "@types/cookie": "^0.5.3", + "@types/cookie": "^0.5.4", "@types/katex": "^0.14.0", "@types/lodash": "^4.14.200", - "@types/lodash.debounce": "^4.0.8", - "@types/object-path": "^0.11.3", - "@types/proxy-from-env": "^1.0.3", - "@types/speakeasy": "^2.0.9", + "@types/lodash.debounce": "^4.0.9", + "@types/object-path": "^0.11.4", + "@types/proxy-from-env": "^1.0.4", + "@types/speakeasy": "^2.0.10", "@xmldom/xmldom": "^0.8.10", - "adm-zip": "0.5.10", + "adm-zip": "0.5.16", "ajv": "^8.11.0", "ajv-formats": "~2.1.1", "apn": "2.2.0", @@ -306,14 +306,14 @@ "aws-sdk": "^2.1363.0", "bad-words": "^3.0.4", "bcrypt": "^5.0.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "bson": "^4.6.4", "busboy": "^1.6.0", "bytebuffer": "5.0.1", "chalk": "^4.0.0", "change-case": "^4.1.2", "chart.js": "^3.8.0", - "codemirror": "^5.65.15", + "codemirror": "^5.65.17", "colorette": "^2.0.20", "colors": "^1.4.0", "connect": "^3.7.0", @@ -321,7 +321,7 @@ "cookie-parser": "^1.4.6", "cors": "^2.8.5", "cron": "~1.8.2", - "css-vars-ponyfill": "^2.4.8", + "css-vars-ponyfill": "^2.4.9", "csv-parse": "^5.2.0", "date-fns": "^2.28.0", "date.js": "~0.3.3", @@ -342,7 +342,7 @@ "filenamify": "^4.3.0", "filesize": "9.0.11", "generate-password": "^1.7.1", - "google-libphonenumber": "^3.2.33", + "google-libphonenumber": "^3.2.38", "gravatar": "^1.8.2", "he": "^1.2.0", "highlight.js": "^11.6.0", @@ -361,7 +361,7 @@ "jsdom": "^16.7.0", "jsrsasign": "^10.5.24", "juice": "^8.0.0", - "katex": "~0.16.9", + "katex": "~0.16.11", "ldap-escape": "^2.0.6", "ldapjs": "^2.3.3", "limax": "^3.0.0", @@ -372,13 +372,13 @@ "mailparser": "^3.4.0", "marked": "^4.2.5", "mem": "^8.1.1", - "meteor-node-stubs": "^1.2.5", + "meteor-node-stubs": "^1.2.10", "mime-db": "^1.52.0", "mime-type": "^4.0.0", "mkdirp": "^1.0.4", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "moment": "^2.29.4", - "moment-timezone": "^0.5.43", + "moment-timezone": "^0.5.45", "mongo-message-queue": "^1.0.0", "mongodb": "^4.17.2", "nats": "^2.6.1", @@ -402,7 +402,7 @@ "query-string": "^7.1.3", "queue-fifo": "^0.2.6", "rc-scrollbars": "^1.1.6", - "re-resizable": "^6.9.9", + "re-resizable": "^6.9.18", "react": "~17.0.2", "react-aria": "~3.23.1", "react-dom": "~17.0.2", @@ -419,7 +419,7 @@ "sodium-native": "^3.3.0", "sodium-plus": "^0.9.0", "speakeasy": "^2.0.0", - "stream-buffers": "^3.0.2", + "stream-buffers": "^3.0.3", "strict-uri-encode": "^2.0.0", "string-strip-html": "^7.0.3", "suretype": "~2.4.1", @@ -431,15 +431,15 @@ "twilio": "^3.76.1", "twit": "^2.2.11", "typia": "~6.9.0", - "ua-parser-js": "^1.0.37", - "underscore": "^1.13.6", + "ua-parser-js": "^1.0.38", + "underscore": "^1.13.7", "universal-perf-hooks": "^1.0.1", "url-polyfill": "^1.1.12", "use-subscription": "~1.6.0", - "use-sync-external-store": "^1.2.0", + "use-sync-external-store": "^1.2.2", "uuid": "^8.3.2", "vm2": "^3.9.19", - "webdav": "^4.11.3", + "webdav": "^4.11.4", "xml-crypto": "~3.1.0", "xml-encryption": "~3.0.2", "xml2js": "~0.5.0", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 233e71b719e5..791526c46f9d 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -38,19 +38,19 @@ "react-split-pane": "^0.1.92", "react-virtuoso": "^4.7.1", "reactflow": "^11.7.2", - "use-subscription": "^1.8.0" + "use-subscription": "^1.8.2" }, "devDependencies": { - "@types/react": "~17.0.69", - "@types/react-beautiful-dnd": "^13.1.6", - "@types/react-dom": "~17.0.22", - "@types/use-subscription": "^1.0.1", + "@types/react": "~17.0.80", + "@types/react-beautiful-dnd": "^13.1.8", + "@types/react-dom": "~17.0.25", + "@types/use-subscription": "^1.0.2", "@typescript-eslint/eslint-plugin": "~5.60.1", "@typescript-eslint/parser": "~5.60.1", "@vitejs/plugin-react": "^4.0.0", "eslint": "~8.45.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.4", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.11", "typescript": "~5.5.4", "vite": "^4.3.9" }, diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 8143df3577a7..b9e45ed14eda 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -29,9 +29,9 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "mem": "^8.1.1", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "mongodb": "^4.17.2", "nats": "^2.4.0", "pino": "^8.15.0", @@ -40,11 +40,11 @@ }, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "@types/bcrypt": "^5.0.1", - "@types/gc-stats": "^1.4.2", - "@types/polka": "^0.5.6", + "@types/bcrypt": "^5.0.2", + "@types/gc-stats": "^1.4.3", + "@types/polka": "^0.5.7", "eslint": "~8.45.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/ee/apps/account-service/src/service.js", diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index c4c0ec82f507..b7912b8bc94d 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -27,9 +27,9 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "mem": "^8.1.1", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "mongodb": "^4.17.2", "nats": "^2.4.0", "pino": "^8.15.0", @@ -37,10 +37,10 @@ }, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "@types/gc-stats": "^1.4.2", - "@types/polka": "^0.5.6", + "@types/gc-stats": "^1.4.3", + "@types/polka": "^0.5.7", "eslint": "~8.45.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/ee/apps/authorization-service/src/service.js", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index be56cb76469c..87029e4b8993 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -30,33 +30,33 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "jaeger-client": "^3.19.0", "mem": "^8.1.1", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "mongodb": "^4.17.2", "nats": "^2.4.0", "pino": "^8.15.0", "polka": "^0.5.2", "sharp": "^0.32.6", - "underscore": "^1.13.6", + "underscore": "^1.13.7", "uuid": "^7.0.3", "ws": "^8.8.1" }, "devDependencies": { "@rocket.chat/ddp-client": "workspace:~", "@rocket.chat/eslint-config": "workspace:^", - "@types/ejson": "^2.2.1", - "@types/gc-stats": "^1.4.2", + "@types/ejson": "^2.2.2", + "@types/gc-stats": "^1.4.3", "@types/meteor": "^2.9.8", "@types/node": "^14.18.63", - "@types/polka": "^0.5.6", + "@types/polka": "^0.5.7", "@types/sharp": "^0.30.5", "@types/uuid": "^8.3.4", - "@types/ws": "^8.5.8", + "@types/ws": "^8.5.12", "eslint": "~8.45.0", "pino-pretty": "^7.6.1", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/service.js", diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 0a8ba5e9f8cc..27290070b653 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -31,10 +31,10 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "mem": "^8.1.1", - "moleculer": "^0.14.31", - "moment-timezone": "^0.5.43", + "moleculer": "^0.14.34", + "moment-timezone": "^0.5.45", "mongo-message-queue": "^1.0.0", "mongodb": "^4.17.2", "nats": "^2.4.0", @@ -43,10 +43,10 @@ }, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "@types/gc-stats": "^1.4.2", - "@types/polka": "^0.5.6", + "@types/gc-stats": "^1.4.3", + "@types/polka": "^0.5.7", "eslint": "~8.45.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/ee/apps/omnichannel-transcript/src/service.js", diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 5ab5a6238266..5968631341b0 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -27,9 +27,9 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "mem": "^8.1.1", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "mongodb": "^4.17.2", "nats": "^2.4.0", "pino": "^8.15.0", @@ -37,10 +37,10 @@ }, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "@types/gc-stats": "^1.4.2", - "@types/polka": "^0.5.6", + "@types/gc-stats": "^1.4.3", + "@types/polka": "^0.5.7", "eslint": "~8.45.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/ee/apps/presence-service/src/service.js", diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 7844b0c27693..b3d8b0aff94e 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -29,10 +29,10 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "mem": "^8.1.1", - "moleculer": "^0.14.31", - "moment-timezone": "^0.5.43", + "moleculer": "^0.14.34", + "moment-timezone": "^0.5.45", "mongo-message-queue": "^1.0.0", "mongodb": "^4.17.2", "nats": "^2.4.0", @@ -41,10 +41,10 @@ }, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", - "@types/gc-stats": "^1.4.2", - "@types/polka": "^0.5.6", + "@types/gc-stats": "^1.4.3", + "@types/polka": "^0.5.7", "eslint": "~8.45.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/ee/apps/queue-worker/src/service.js", diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 030371478cb2..9fd5cf0586f6 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -27,9 +27,9 @@ "event-loop-stats": "^1.4.1", "eventemitter3": "^4.0.7", "fibers": "^5.0.3", - "gc-stats": "^1.4.0", + "gc-stats": "^1.4.1", "mem": "^8.1.1", - "moleculer": "^0.14.31", + "moleculer": "^0.14.34", "mongodb": "^4.17.2", "nats": "^2.4.0", "pino": "^8.15.0", @@ -38,11 +38,11 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", - "@types/bcrypt": "^5.0.1", - "@types/gc-stats": "^1.4.2", - "@types/polka": "^0.5.6", + "@types/bcrypt": "^5.0.2", + "@types/gc-stats": "^1.4.3", + "@types/polka": "^0.5.7", "eslint": "~8.45.0", - "ts-node": "^10.9.1", + "ts-node": "^10.9.2", "typescript": "~5.5.4" }, "main": "./dist/ee/apps/stream-hub-service/src/service.js", diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 0b66c13574c2..3eceb35e27ff 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -4,9 +4,9 @@ "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/bcrypt": "^5.0.1", - "@types/jest": "~29.5.12", - "@types/ws": "^8.5.8", + "@types/bcrypt": "^5.0.2", + "@types/jest": "~29.5.13", + "@types/ws": "^8.5.12", "eslint": "~8.45.0", "jest": "~29.7.0", "jest-websocket-mock": "~2.5.0", diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 6df90c641375..696ac6fee640 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "~29.7.0", "typescript": "~5.5.4" @@ -28,7 +28,7 @@ "eventemitter3": "^4.0.7", "fibers": "^5.0.3", "mem": "^8.1.1", - "moment-timezone": "^0.5.43", + "moment-timezone": "^0.5.45", "mongo-message-queue": "^1.0.0", "mongodb": "^4.17.2", "pino": "^8.15.0" diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index afd5a4d8e314..234463087a4e 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -6,11 +6,11 @@ "@rocket.chat/jest-presets": "workspace:~", "@storybook/addon-essentials": "~6.5.16", "@storybook/react": "~6.5.16", - "@testing-library/react": "~16.0.0", - "@types/emojione": "^2.2.8", - "@types/jest": "~29.5.12", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", + "@testing-library/react": "~16.0.1", + "@types/emojione": "^2.2.9", + "@types/jest": "~29.5.13", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", "eslint": "~8.45.0", "jest": "~29.7.0", "react-dom": "~18.3.1", @@ -38,7 +38,7 @@ "emoji-assets": "^7.0.1", "emoji-toolkit": "^7.0.1", "moment": "^2.29.4", - "moment-timezone": "^0.5.43", + "moment-timezone": "^0.5.45", "react": "~18.3.1" }, "volta": { diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 5f3bb4977d7d..d6e3e93c01a4 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -8,14 +8,14 @@ "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/ui-contexts": "workspace:~", - "@types/react": "~17.0.69", + "@types/react": "~17.0.80", "eslint": "~8.45.0", "eslint-plugin-anti-trojan-source": "~1.1.1", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-testing-library": "^5.11.1", "react": "~17.0.2", - "react-docgen-typescript-plugin": "~1.0.5", + "react-docgen-typescript-plugin": "~1.0.8", "typescript": "~5.5.4" }, "scripts": { diff --git a/package.json b/package.json index 405686417d28..f3e3ce3d3e91 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ }, "devDependencies": { "@changesets/cli": "^2.26.2", - "@types/chart.js": "^2.9.39", - "@types/js-yaml": "^4.0.8", + "@types/chart.js": "^2.9.41", + "@types/js-yaml": "^4.0.9", "ts-node": "^10.9.2", - "turbo": "latest" + "turbo": "^2.1.2" }, "workspaces": [ "apps/*", @@ -65,7 +65,8 @@ "mongodb@^4.17.1": "patch:mongodb@npm:4.17.1#.yarn/patches/mongodb-npm-4.17.1-a2fe811ff1.patch", "@rocket.chat/forked-matrix-sdk-crypto-nodejs": "0.1.0-beta.13", "typia@~6.9.0": "patch:typia@npm%3A6.9.0#./.yarn/patches/typia-npm-6.9.0-2fd4d85f25.patch", - "react-i18next@~15.0.1": "patch:react-i18next@npm%3A15.0.1#./.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch" + "react-i18next@~15.0.1": "patch:react-i18next@npm%3A15.0.1#./.yarn/patches/react-i18next-npm-15.0.1-0812bb73aa.patch", + "moleculer@^0.14.34": "patch:moleculer@npm%3A0.14.34#./.yarn/patches/moleculer-npm-0.14.34-440e26767d.patch" }, "dependencies": { "node-gyp": "^9.4.1" diff --git a/packages/agenda/package.json b/packages/agenda/package.json index f09921de6c31..2d1fc084c383 100644 --- a/packages/agenda/package.json +++ b/packages/agenda/package.json @@ -8,11 +8,11 @@ "date.js": "~0.3.3", "debug": "~4.1.1", "human-interval": "^2.0.1", - "moment-timezone": "~0.5.43", + "moment-timezone": "~0.5.45", "mongodb": "^4.17.2" }, "devDependencies": { - "@types/debug": "^4.1.10", + "@types/debug": "^4.1.12", "eslint": "~8.45.0", "typescript": "~5.5.4" }, diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 23e0d7233a0f..b8d9ac155144 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -3,8 +3,8 @@ "version": "0.2.6", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", - "@types/strict-uri-encode": "^2.0.1", + "@types/jest": "~29.5.13", + "@types/strict-uri-encode": "^2.0.2", "eslint": "~8.45.0", "jest": "~29.7.0", "jest-fetch-mock": "^3.0.3", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 35759dbe5ee6..74250ffa8c16 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -8,9 +8,7 @@ "@babel/preset-typescript": "~7.22.15", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", - "@types/babel__core": "^7.20.3", - "@types/babel__preset-env": "^7.9.4", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "babel-jest": "^29.5.0", "eslint": "~8.45.0", "jest": "~29.7.0", @@ -41,7 +39,7 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", - "@types/fibers": "^3.1.3", + "@types/fibers": "^3.1.4", "fibers": "^5.0.3" } } diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 6f3f29871ccf..0fc47dc9a122 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -3,8 +3,8 @@ "version": "0.3.6", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", - "@types/ws": "^8.5.8", + "@types/jest": "~29.5.13", + "@types/ws": "^8.5.12", "eslint": "~8.45.0", "jest": "~29.7.0", "jest-websocket-mock": "~2.5.0", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index c0fa20f58227..4915221b71ca 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -4,8 +4,8 @@ "description": "Rocket.Chat's JS/TS ESLint config", "dependencies": { "@babel/core": "^7.20.7", - "@babel/eslint-parser": "~7.23.3", - "@types/eslint": "~8.44.6", + "@babel/eslint-parser": "~7.23.10", + "@types/eslint": "~8.44.9", "@types/prettier": "^2.6.3", "@typescript-eslint/eslint-plugin": "~5.60.1", "@typescript-eslint/parser": "~5.60.1", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 43a79f90b9ed..80874491a951 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -86,12 +86,10 @@ "@storybook/source-loader": "~6.5.16", "@storybook/theming": "~6.5.16", "@tanstack/react-query": "^4.16.1", - "@testing-library/react": "~16.0.0", + "@testing-library/react": "~16.0.1", "@testing-library/user-event": "~14.5.2", - "@types/babel__core": "^7.20.3", - "@types/babel__preset-env": "^7.9.4", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", "babel-loader": "~8.2.5", "cross-env": "^7.0.3", "eslint": "~8.45.0", @@ -100,11 +98,11 @@ "normalize.css": "^8.0.1", "npm-run-all": "^4.1.5", "prettier": "~2.8.8", - "react-docgen-typescript-plugin": "~1.0.5", + "react-docgen-typescript-plugin": "~1.0.8", "react-dom": "^17.0.2", "react-i18next": "~15.0.1", "rimraf": "^3.0.2", - "storybook-dark-mode": "~3.0.1", + "storybook-dark-mode": "~3.0.3", "tslib": "^2.5.3", "typescript": "~5.5.4" }, diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index babdc60161da..c2fbf136b2d6 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -44,25 +44,25 @@ "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.16", "@storybook/testing-library": "~0.0.13", - "@testing-library/react": "~16.0.0", + "@testing-library/react": "~16.0.1", "@types/dompurify": "^3.0.5", - "@types/jest": "~29.5.12", - "@types/katex": "~0.16.5", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", + "@types/jest": "~29.5.13", + "@types/katex": "~0.16.7", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", "@typescript-eslint/eslint-plugin": "~5.60.1", "@typescript-eslint/parser": "~5.60.1", "babel-loader": "^8.3.0", "eslint": "~8.45.0", "eslint-plugin-anti-trojan-source": "~1.1.1", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-storybook": "~0.6.15", "identity-obj-proxy": "^3.0.0", "jest": "~29.7.0", - "katex": "~0.16.9", + "katex": "~0.16.11", "outdent": "^0.8.0", - "react-docgen-typescript-plugin": "~1.0.5", + "react-docgen-typescript-plugin": "~1.0.8", "react-dom": "~17.0.2", "react-i18next": "~15.0.1", "typescript": "~5.5.4" diff --git a/packages/jest-presets/package.json b/packages/jest-presets/package.json index 078d19289d34..64a511b3de5c 100644 --- a/packages/jest-presets/package.json +++ b/packages/jest-presets/package.json @@ -13,7 +13,7 @@ "/server" ], "dependencies": { - "@swc/core": "~1.7.23", + "@swc/core": "~1.7.26", "@swc/jest": "~0.2.36", "@testing-library/jest-dom": "~6.4.8", "@types/jest-axe": "~3.5.9", @@ -25,9 +25,9 @@ }, "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", - "@types/identity-obj-proxy": "^3", - "@types/jest": "~29.5.12", - "@types/uuid": "^9", + "@types/identity-obj-proxy": "^3.0.2", + "@types/jest": "~29.5.13", + "@types/uuid": "^9.0.8", "eslint": "~8.45.0", "jest": "~29.7.0", "typescript": "~5.5.4" diff --git a/packages/jwt/package.json b/packages/jwt/package.json index d945bd3a6f9c..7235a916385d 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "~29.7.0", "typescript": "~5.5.4" diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 5887c40028c4..ce7bb92d6b78 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -25,7 +25,7 @@ "typecheck": "tsc -p tsconfig.typecheck.json" }, "devDependencies": { - "@babel/eslint-parser": "~7.23.3", + "@babel/eslint-parser": "~7.23.10", "@babel/preset-env": "~7.22.20", "@babel/preset-typescript": "~7.22.15", "@rocket.chat/core-typings": "workspace:^", @@ -42,7 +42,7 @@ "@types/crypto-js": "~4.1.3", "@types/markdown-it": "^14.0.1", "@types/mini-css-extract-plugin": "~1.4.3", - "@types/webpack": "^5.28.4", + "@types/webpack": "^5.28.5", "@types/webpack-bundle-analyzer": "^4.6.2", "@types/webpack-dev-server": "~4.7.2", "@types/whatwg-fetch": "~0.0.33", @@ -57,7 +57,7 @@ "eslint": "~8.45.0", "eslint-plugin-import": "~2.26.0", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "file-loader": "^6.2.0", "gh-release": "^3.5.0", "html-webpack-plugin": "^5.5.3", @@ -102,7 +102,7 @@ "@rocket.chat/random": "workspace:~", "@rocket.chat/sdk": "^1.0.0-alpha.42", "@rocket.chat/ui-kit": "workspace:~", - "css-vars-ponyfill": "^2.4.8", + "css-vars-ponyfill": "^2.4.9", "date-fns": "^2.15.0", "emoji-mart": "^3.0.1", "history": "~5.3.0", @@ -116,8 +116,8 @@ "query-string": "^7.1.3", "react-hook-form": "~7.45.4", "react-i18next": "~15.0.1", - "storybook-dark-mode": "~3.0.1", - "whatwg-fetch": "^3.6.19" + "storybook-dark-mode": "~3.0.3", + "whatwg-fetch": "^3.6.20" }, "browserslist": [ "last 2 versions", diff --git a/packages/log-format/package.json b/packages/log-format/package.json index f6f7c03b6f63..a999c7d884f0 100644 --- a/packages/log-format/package.json +++ b/packages/log-format/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@types/chalk": "^2.2.0", - "@types/ejson": "^2.2.1", + "@types/ejson": "^2.2.2", "eslint": "~8.45.0", "typescript": "~5.5.4" }, diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index 78ffd3401ff4..e58727ba38c2 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -48,29 +48,29 @@ "docs": "typedoc" }, "devDependencies": { - "@babel/core": "~7.21.4", - "@babel/eslint-parser": "~7.21.3", - "@babel/preset-env": "~7.21.4", + "@babel/core": "~7.21.8", + "@babel/eslint-parser": "~7.21.8", + "@babel/preset-env": "~7.21.5", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/peggy-loader": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", - "@types/jest": "~29.5.12", - "@types/node": "~14.18.42", + "@types/jest": "~29.5.13", + "@types/node": "~14.18.63", "@typescript-eslint/parser": "~5.58.0", - "babel-loader": "~9.1.2", + "babel-loader": "~9.1.3", "eslint": "~8.45.0", "jest": "~29.7.0", "npm-run-all": "^4.1.5", "peggy": "3.0.2", - "prettier": "~2.8.7", + "prettier": "~2.8.8", "prettier-plugin-pegjs": "~0.5.4", "rimraf": "^3.0.2", - "ts-loader": "~9.4.2", - "typedoc": "~0.24.1", + "ts-loader": "~9.4.4", + "typedoc": "~0.24.8", "typescript": "~5.5.4", "webpack": "~5.78.0", - "webpack-cli": "~5.0.1" + "webpack-cli": "~5.0.2" }, "dependencies": { "tldts": "~5.7.112" diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index fa8c98cfbc5c..f9e9b4d06ab8 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -14,7 +14,7 @@ "@rocket.chat/ui-contexts": "workspace:*", "@storybook/react": "~6.5.16", "@tanstack/react-query": "^4.16.1", - "@types/use-sync-external-store": "^0.0.5", + "@types/use-sync-external-store": "^0.0.6", "eslint": "~8.45.0", "react": "~17.0.2", "typescript": "~5.5.4" diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index e1df06d2ba3c..30144dabb49a 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -3,7 +3,7 @@ "version": "0.7.0", "private": true, "devDependencies": { - "@types/node-rsa": "^1.1.3", + "@types/node-rsa": "^1.1.4", "eslint": "~8.45.0", "mongodb": "^4.17.2", "typescript": "~5.5.4" diff --git a/packages/models/package.json b/packages/models/package.json index f64268e12a5b..e55ce8d5a0e1 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "^29.7.0", "typescript": "~5.5.4" diff --git a/packages/password-policies/package.json b/packages/password-policies/package.json index a2fef4ae0543..6bfb0d6100f1 100644 --- a/packages/password-policies/package.json +++ b/packages/password-policies/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "~29.7.0", "typescript": "~5.5.4" diff --git a/packages/patch-injection/package.json b/packages/patch-injection/package.json index 8dabf2fb3f2a..2ac35749c481 100644 --- a/packages/patch-injection/package.json +++ b/packages/patch-injection/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "~29.7.0", "typescript": "~5.5.4" diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index f0ac6d915567..6eb584f62d24 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -44,11 +44,11 @@ "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", - "@types/node": "~14.18.42", + "@types/node": "~14.18.63", "eslint": "~8.45.0", "npm-run-all": "^4.1.5", "peggy": "3.0.2", - "prettier": "~2.8.7", + "prettier": "~2.8.8", "rimraf": "^3.0.2", "typescript": "~5.5.4", "webpack": "~5.78.0" diff --git a/packages/release-action/package.json b/packages/release-action/package.json index e633ef69d06f..0851b8b43fca 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -10,7 +10,7 @@ "main": "dist/index.js", "packageManager": "yarn@3.5.1", "devDependencies": { - "@types/node": "^16.18.60", + "@types/node": "^16.18.108", "typescript": "~5.5.4" }, "dependencies": { diff --git a/packages/release-changelog/package.json b/packages/release-changelog/package.json index f183d00b550b..222f7f20355a 100644 --- a/packages/release-changelog/package.json +++ b/packages/release-changelog/package.json @@ -16,6 +16,6 @@ }, "dependencies": { "dataloader": "^1.4.0", - "node-fetch": "^2" + "node-fetch": "^2.7.0" } } diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 61e23c4c93ce..972577210f56 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -3,7 +3,7 @@ "version": "6.13.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "~29.7.0", "mongodb": "^4.17.2", diff --git a/packages/server-fetch/package.json b/packages/server-fetch/package.json index 27f957758520..e09b20d1aa42 100644 --- a/packages/server-fetch/package.json +++ b/packages/server-fetch/package.json @@ -21,7 +21,7 @@ "extends": "../../package.json" }, "dependencies": { - "@types/proxy-from-env": "^1.0.3", + "@types/proxy-from-env": "^1.0.4", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "node-fetch": "2.3.0", diff --git a/packages/tools/package.json b/packages/tools/package.json index 11dade3ac5c8..bb38a8c5ae4f 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "eslint": "~8.45.0", "jest": "~29.7.0", "typescript": "~5.5.4" @@ -24,6 +24,6 @@ "/dist" ], "dependencies": { - "moment-timezone": "^0.5.43" + "moment-timezone": "^0.5.45" } } diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 42860fd8692a..0423b68f2b43 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -6,12 +6,11 @@ "@babel/core": "~7.22.20", "@rocket.chat/fuselage": "^0.59.0", "@rocket.chat/ui-contexts": "workspace:^", - "@types/babel__core": "~7.20.3", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", "eslint": "~8.45.0", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-storybook": "~0.6.15", "eslint-plugin-testing-library": "~5.11.1", "react": "^17.0.2", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a2b916b68241..a84317f37abc 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -26,6 +26,7 @@ "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/mock-providers": "workspace:^", + "@rocket.chat/ui-avatar": "workspace:~", "@rocket.chat/ui-contexts": "workspace:~", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", @@ -37,15 +38,14 @@ "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.16", "@storybook/testing-library": "~0.0.13", - "@testing-library/react": "~16.0.0", - "@types/babel__core": "~7.20.3", - "@types/jest": "~29.5.12", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", + "@testing-library/react": "~16.0.1", + "@types/jest": "~29.5.13", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", "eslint": "~8.45.0", "eslint-plugin-anti-trojan-source": "~1.1.1", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-storybook": "~0.6.15", "eslint-plugin-testing-library": "~5.11.1", "jest": "~29.7.0", @@ -61,6 +61,7 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", + "@rocket.chat/ui-avatar": "*", "@rocket.chat/ui-contexts": "*", "react": "*", "react-i18next": "*" diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 55e22a79e34c..0f97fef22508 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -28,15 +28,14 @@ "@storybook/manager-webpack4": "~6.5.16", "@storybook/react": "~6.5.16", "@storybook/testing-library": "~0.0.13", - "@types/babel__core": "~7.20.3", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", "eslint": "~8.45.0", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-storybook": "~0.6.15", "react": "~17.0.2", - "react-docgen-typescript-plugin": "~1.0.5", + "react-docgen-typescript-plugin": "~1.0.8", "react-dom": "~17.0.2", "typescript": "~5.5.4" }, diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 24c8b5de5ce2..3ef588432a7c 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -9,15 +9,15 @@ "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/i18n": "workspace:~", "@rocket.chat/rest-typings": "workspace:^", - "@types/react": "~17.0.69", - "@types/react-dom": "~17.0.22", - "@types/use-sync-external-store": "^0.0.5", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", + "@types/use-sync-external-store": "^0.0.6", "eslint": "~8.45.0", - "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-hooks": "^4.6.2", "mongodb": "^4.17.2", "react": "~17.0.2", "typescript": "~5.5.4", - "use-sync-external-store": "^1.2.0" + "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 9d39ae97ac55..ae50fbc17168 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -35,23 +35,23 @@ "test": "jest" }, "devDependencies": { - "@babel/core": "~7.21.4", - "@babel/eslint-parser": "~7.23.3", + "@babel/core": "~7.21.8", + "@babel/eslint-parser": "~7.23.10", "@babel/plugin-transform-runtime": "~7.21.4", - "@babel/preset-env": "~7.21.4", + "@babel/preset-env": "~7.21.5", "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", - "@types/jest": "~29.5.12", - "babel-loader": "~9.1.2", + "@types/jest": "~29.5.13", + "babel-loader": "~9.1.3", "eslint": "~8.45.0", "jest": "~29.7.0", "npm-run-all": "~4.1.5", "prettier": "~2.8.8", "rimraf": "~3.0.2", - "ts-jest": "~29.1.1", - "ts-loader": "~9.4.2", - "ts-node": "~10.9.1", + "ts-jest": "~29.1.5", + "ts-loader": "~9.4.4", + "ts-node": "~10.9.2", "ts-patch": "~3.2.1", "typescript": "~5.5.4" }, diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index d10c05edad2c..eb363da1ceae 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -22,16 +22,15 @@ "@storybook/react": "~6.5.16", "@storybook/testing-library": "~0.0.13", "@storybook/testing-react": "~1.3.0", - "@types/babel__core": "~7.20.3", - "@types/jest": "~29.5.12", + "@types/jest": "~29.5.13", "@types/jest-axe": "~3.5.9", "eslint": "~8.45.0", "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.0", + "eslint-plugin-react-hooks": "~4.6.2", "eslint-plugin-storybook": "~0.6.15", "jest": "~29.7.0", "jest-axe": "~9.0.0", - "react-docgen-typescript-plugin": "~1.0.5", + "react-docgen-typescript-plugin": "~1.0.8", "typescript": "~5.5.4" }, "peerDependencies": { diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 71e015bb82c0..ae08e05ee218 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -21,7 +21,7 @@ "@babel/preset-react": "~7.22.15", "@babel/preset-typescript": "~7.22.15", "@rocket.chat/i18n": "workspace:~", - "@rocket.chat/layout": "~0.31.26", + "@rocket.chat/layout": "~0.31.27", "@rocket.chat/mock-providers": "workspace:~", "@rocket.chat/tools": "workspace:~", "@rocket.chat/ui-client": "workspace:^", @@ -34,14 +34,14 @@ "@storybook/react": "~6.5.16", "@storybook/testing-library": "^0.2.2", "@tanstack/react-query": "^4.16.1", - "@testing-library/react": "~16.0.0", - "@types/react": "~17.0.69", + "@testing-library/react": "~16.0.1", + "@types/react": "~17.0.80", "babel-loader": "~8.3.0", "eslint": "~8.45.0", "react": "~17.0.2", "react-hook-form": "~7.45.4", "react-i18next": "~15.0.1", - "storybook-dark-mode": "~3.0.1", + "storybook-dark-mode": "~3.0.3", "typescript": "~5.5.4" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index cd440c92c40f..6b1123701996 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1039,7 +1039,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:~7.21.4": +"@babel/core@npm:~7.21.8": version: 7.21.8 resolution: "@babel/core@npm:7.21.8" dependencies: @@ -1062,7 +1062,7 @@ __metadata: languageName: node linkType: hard -"@babel/eslint-parser@npm:~7.21.3": +"@babel/eslint-parser@npm:~7.21.8": version: 7.21.8 resolution: "@babel/eslint-parser@npm:7.21.8" dependencies: @@ -1076,9 +1076,9 @@ __metadata: languageName: node linkType: hard -"@babel/eslint-parser@npm:~7.23.3": - version: 7.23.3 - resolution: "@babel/eslint-parser@npm:7.23.3" +"@babel/eslint-parser@npm:~7.23.10": + version: 7.23.10 + resolution: "@babel/eslint-parser@npm:7.23.10" dependencies: "@nicolo-ribaudo/eslint-scope-5-internals": 5.1.1-v1 eslint-visitor-keys: ^2.1.0 @@ -1086,7 +1086,7 @@ __metadata: peerDependencies: "@babel/core": ^7.11.0 eslint: ^7.5.0 || ^8.0.0 - checksum: 9573daebe21af5123c302c307be80cacf1c2bf236a9497068a14726d3944ef55e1282519d0ccf51882dfc369359a3442299c98cb22a419e209924db39d4030fd + checksum: 81249edee14f95720044f393b5b0a681a230ac2bde3d656b0c55b1cec4c5cb99ce0584ef6acd2e5413acc7905daee1b2e1db8e3fab18a3a74c508098a584ec9a languageName: node linkType: hard @@ -2733,7 +2733,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:~7.21.4": +"@babel/preset-env@npm:~7.21.5": version: 7.21.5 resolution: "@babel/preset-env@npm:7.21.5" dependencies: @@ -4701,13 +4701,13 @@ __metadata: languageName: node linkType: hard -"@kaciras/deasync@npm:^1.0.3": - version: 1.0.3 - resolution: "@kaciras/deasync@npm:1.0.3" +"@kaciras/deasync@npm:^1.0.4": + version: 1.0.4 + resolution: "@kaciras/deasync@npm:1.0.4" dependencies: - follow-redirects: ^1.15.2 - tar-fs: ^2.1.1 - checksum: 6986db099e8676ae2c21d1122db9f60dd5b74dd4ec06eb164c7ded0f512d037146f82cd3dee189d2863b1c6064f17d33b273edf67b25d3bb71050c5a6fe1aa6b + follow-redirects: ^1.15.6 + tar-fs: ^3.0.5 + checksum: 4bb358de84731000f3576dff029f89bf6ddf8628c95fd3661c029716a796fc44eb3769445479c3f4a0990f10617caf7902bf07547df76bf239582f565d3855fc languageName: node linkType: hard @@ -4858,6 +4858,26 @@ __metadata: languageName: node linkType: hard +"@meteorjs/crypto-browserify@npm:^3.12.1": + version: 3.12.1 + resolution: "@meteorjs/crypto-browserify@npm:3.12.1" + dependencies: + browserify-cipher: ^1.0.1 + browserify-sign: ^4.2.3 + create-ecdh: ^4.0.4 + create-hash: ^1.2.0 + create-hmac: ^1.1.7 + diffie-hellman: ^5.0.3 + hash-base: ~3.0.4 + inherits: ^2.0.4 + pbkdf2: ^3.1.2 + public-encrypt: ^4.0.3 + randombytes: ^2.1.0 + randomfill: ^1.0.4 + checksum: 0e7b3528273d057587a28d5aba644638da966e0d641c9211b16327efa2ce1e69f769d1bfb385cf974eb36e2fd96e81e2a4d75ec4f43bf230df0c4326ed9cdd9e + languageName: node + linkType: hard + "@mongodb-js/saslprep@npm:^1.1.0": version: 1.1.0 resolution: "@mongodb-js/saslprep@npm:1.1.0" @@ -8424,24 +8444,24 @@ __metadata: "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/tools": "workspace:^" - "@types/bcrypt": ^5.0.1 - "@types/gc-stats": ^1.4.2 + "@types/bcrypt": ^5.0.2 + "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 bcrypt: ^5.0.1 ejson: ^2.2.3 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 mem: ^8.1.1 - moleculer: ^0.14.31 + moleculer: ^0.14.34 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 polka: ^0.5.2 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 uuid: ^9.0.1 languageName: unknown @@ -8460,13 +8480,13 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/agenda@workspace:packages/agenda" dependencies: - "@types/debug": ^4.1.10 + "@types/debug": ^4.1.12 cron: ~1.8.2 date.js: ~0.3.3 debug: ~4.1.1 eslint: ~8.45.0 human-interval: ^2.0.1 - moment-timezone: ~0.5.43 + moment-timezone: ~0.5.45 mongodb: ^4.17.2 typescript: ~5.5.4 languageName: unknown @@ -8479,8 +8499,8 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/rest-typings": "workspace:^" - "@types/jest": ~29.5.12 - "@types/strict-uri-encode": ^2.0.1 + "@types/jest": ~29.5.13 + "@types/strict-uri-encode": ^2.0.2 eslint: ~8.45.0 filter-obj: ^3.0.0 jest: ~29.7.0 @@ -8538,22 +8558,22 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 - "@types/gc-stats": ^1.4.2 + "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 ejson: ^2.2.3 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 mem: ^8.1.1 - moleculer: ^0.14.31 + moleculer: ^0.14.34 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 polka: ^0.5.2 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 languageName: unknown linkType: soft @@ -8600,10 +8620,8 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" - "@types/babel__core": ^7.20.3 - "@types/babel__preset-env": ^7.9.4 - "@types/fibers": ^3.1.3 - "@types/jest": ~29.5.12 + "@types/fibers": ^3.1.4 + "@types/jest": ~29.5.13 babel-jest: ^29.5.0 eslint: ~8.45.0 fibers: ^5.0.3 @@ -8675,8 +8693,8 @@ __metadata: "@rocket.chat/core-typings": "workspace:~" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/rest-typings": "workspace:^" - "@types/jest": ~29.5.12 - "@types/ws": ^8.5.8 + "@types/jest": ~29.5.13 + "@types/ws": ^8.5.12 eslint: ~8.45.0 jest: ~29.7.0 jest-websocket-mock: ~2.5.0 @@ -8703,33 +8721,33 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 - "@types/ejson": ^2.2.1 - "@types/gc-stats": ^1.4.2 + "@types/ejson": ^2.2.2 + "@types/gc-stats": ^1.4.3 "@types/meteor": ^2.9.8 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 "@types/sharp": ^0.30.5 "@types/uuid": ^8.3.4 - "@types/ws": ^8.5.8 + "@types/ws": ^8.5.12 colorette: ^1.4.0 ejson: ^2.2.3 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 jaeger-client: ^3.19.0 mem: ^8.1.1 - moleculer: ^0.14.31 + moleculer: ^0.14.34 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 pino-pretty: ^7.6.1 polka: ^0.5.2 sharp: ^0.32.6 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 - underscore: ^1.13.6 + underscore: ^1.13.7 uuid: ^7.0.3 ws: ^8.8.1 languageName: unknown @@ -8747,8 +8765,8 @@ __metadata: resolution: "@rocket.chat/eslint-config@workspace:packages/eslint-config" dependencies: "@babel/core": ^7.20.7 - "@babel/eslint-parser": ~7.23.3 - "@types/eslint": ~8.44.6 + "@babel/eslint-parser": ~7.23.10 + "@types/eslint": ~8.44.9 "@types/prettier": ^2.6.3 "@typescript-eslint/eslint-plugin": ~5.60.1 "@typescript-eslint/parser": ~5.60.1 @@ -8913,12 +8931,10 @@ __metadata: "@storybook/source-loader": ~6.5.16 "@storybook/theming": ~6.5.16 "@tanstack/react-query": ^4.16.1 - "@testing-library/react": ~16.0.0 + "@testing-library/react": ~16.0.1 "@testing-library/user-event": ~14.5.2 - "@types/babel__core": ^7.20.3 - "@types/babel__preset-env": ^7.9.4 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 babel-loader: ~8.2.5 cross-env: ^7.0.3 eslint: ~8.45.0 @@ -8927,11 +8943,11 @@ __metadata: normalize.css: ^8.0.1 npm-run-all: ^4.1.5 prettier: ~2.8.8 - react-docgen-typescript-plugin: ~1.0.5 + react-docgen-typescript-plugin: ~1.0.8 react-dom: ^17.0.2 react-i18next: ~15.0.1 rimraf: ^3.0.2 - storybook-dark-mode: ~3.0.1 + storybook-dark-mode: ~3.0.3 tslib: ^2.5.3 typescript: ~5.5.4 peerDependencies: @@ -9000,12 +9016,12 @@ __metadata: "@storybook/manager-webpack4": ~6.5.16 "@storybook/react": ~6.5.16 "@storybook/testing-library": ~0.0.13 - "@testing-library/react": ~16.0.0 + "@testing-library/react": ~16.0.1 "@types/dompurify": ^3.0.5 - "@types/jest": ~29.5.12 - "@types/katex": ~0.16.5 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 + "@types/jest": ~29.5.13 + "@types/katex": ~0.16.7 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 "@typescript-eslint/eslint-plugin": ~5.60.1 "@typescript-eslint/parser": ~5.60.1 babel-loader: ^8.3.0 @@ -9014,14 +9030,14 @@ __metadata: eslint: ~8.45.0 eslint-plugin-anti-trojan-source: ~1.1.1 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-storybook: ~0.6.15 highlight.js: ^11.5.1 identity-obj-proxy: ^3.0.0 jest: ~29.7.0 - katex: ~0.16.9 + katex: ~0.16.11 outdent: ^0.8.0 - react-docgen-typescript-plugin: ~1.0.5 + react-docgen-typescript-plugin: ~1.0.8 react-dom: ~17.0.2 react-error-boundary: ^3.1.4 react-i18next: ~15.0.1 @@ -9079,13 +9095,13 @@ __metadata: resolution: "@rocket.chat/jest-presets@workspace:packages/jest-presets" dependencies: "@rocket.chat/eslint-config": "workspace:~" - "@swc/core": ~1.7.23 + "@swc/core": ~1.7.26 "@swc/jest": ~0.2.36 "@testing-library/jest-dom": ~6.4.8 - "@types/identity-obj-proxy": ^3 - "@types/jest": ~29.5.12 + "@types/identity-obj-proxy": ^3.0.2 + "@types/jest": ~29.5.13 "@types/jest-axe": ~3.5.9 - "@types/uuid": ^9 + "@types/uuid": ^9.0.8 eslint: ~8.45.0 identity-obj-proxy: ~3.0.0 jest: ~29.7.0 @@ -9102,7 +9118,7 @@ __metadata: resolution: "@rocket.chat/jwt@workspace:packages/jwt" dependencies: "@rocket.chat/jest-presets": "workspace:~" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 eslint: ~8.45.0 jest: ~29.7.0 jose: ^4.14.4 @@ -9110,15 +9126,15 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/layout@npm:~0.31.26": - version: 0.31.26 - resolution: "@rocket.chat/layout@npm:0.31.26" +"@rocket.chat/layout@npm:~0.31.27": + version: 0.31.27 + resolution: "@rocket.chat/layout@npm:0.31.27" peerDependencies: "@rocket.chat/fuselage": "*" react: 17.0.2 react-dom: 17.0.2 react-i18next: ~11.15.4 - checksum: 473b3ce43f7e5c495bbbfb54c628a1da9fda672fee0aeef74a25690a462b243982749bb1ba6933130381aea8aae61fb9aed6b45d6c3ec370c7555e1ac69930ce + checksum: 5bf14cc2c85947c5773b9dbaeb7b79f36b770c80673f7eb5aaded80a3f8a7ebcbfb67c8d8c2f4731c630737784b375bd775cc53941f4ce8dd1b9d1a327449b02 languageName: node linkType: hard @@ -9130,9 +9146,9 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jwt": "workspace:^" "@rocket.chat/logger": "workspace:^" - "@types/bcrypt": ^5.0.1 - "@types/jest": ~29.5.12 - "@types/ws": ^8.5.8 + "@types/bcrypt": ^5.0.2 + "@types/jest": ~29.5.13 + "@types/ws": ^8.5.12 bcrypt: ^5.0.1 eslint: ~8.45.0 jest: ~29.7.0 @@ -9145,7 +9161,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/livechat@workspace:packages/livechat" dependencies: - "@babel/eslint-parser": ~7.23.3 + "@babel/eslint-parser": ~7.23.10 "@babel/preset-env": ~7.22.20 "@babel/preset-typescript": ~7.22.15 "@rocket.chat/core-typings": "workspace:^" @@ -9167,7 +9183,7 @@ __metadata: "@types/crypto-js": ~4.1.3 "@types/markdown-it": ^14.0.1 "@types/mini-css-extract-plugin": ~1.4.3 - "@types/webpack": ^5.28.4 + "@types/webpack": ^5.28.5 "@types/webpack-bundle-analyzer": ^4.6.2 "@types/webpack-dev-server": ~4.7.2 "@types/whatwg-fetch": ~0.0.33 @@ -9177,7 +9193,7 @@ __metadata: babel-loader: ^8.3.0 cross-env: ^7.0.3 css-loader: ^4.3.0 - css-vars-ponyfill: ^2.4.8 + css-vars-ponyfill: ^2.4.9 cssnano: ^4.1.11 date-fns: ^2.15.0 desvg-loader: ^0.1.0 @@ -9185,7 +9201,7 @@ __metadata: eslint: ~8.45.0 eslint-plugin-import: ~2.26.0 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 file-loader: ^6.2.0 gh-release: ^3.5.0 history: ~5.3.0 @@ -9219,7 +9235,7 @@ __metadata: sass: ~1.62.1 sass-loader: ~10.4.1 serve: ^11.3.2 - storybook-dark-mode: ~3.0.1 + storybook-dark-mode: ~3.0.3 style-loader: ^1.2.1 stylelint: ^14.9.1 stylelint-order: ^5.0.0 @@ -9231,7 +9247,7 @@ __metadata: webpack-bundle-analyzer: ^4.9.1 webpack-cli: ^5.1.4 webpack-dev-server: ~4.13.3 - whatwg-fetch: ^3.6.19 + whatwg-fetch: ^3.6.20 peerDependencies: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/logo": "*" @@ -9243,7 +9259,7 @@ __metadata: resolution: "@rocket.chat/log-format@workspace:packages/log-format" dependencies: "@types/chalk": ^2.2.0 - "@types/ejson": ^2.2.1 + "@types/ejson": ^2.2.2 chalk: ^4.0.0 ejson: ^2.2.3 eslint: ~8.45.0 @@ -9286,30 +9302,30 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/message-parser@workspace:packages/message-parser" dependencies: - "@babel/core": ~7.21.4 - "@babel/eslint-parser": ~7.21.3 - "@babel/preset-env": ~7.21.4 + "@babel/core": ~7.21.8 + "@babel/eslint-parser": ~7.21.8 + "@babel/preset-env": ~7.21.5 "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/peggy-loader": "workspace:~" "@rocket.chat/prettier-config": ~0.31.25 - "@types/jest": ~29.5.12 - "@types/node": ~14.18.42 + "@types/jest": ~29.5.13 + "@types/node": ~14.18.63 "@typescript-eslint/parser": ~5.58.0 - babel-loader: ~9.1.2 + babel-loader: ~9.1.3 eslint: ~8.45.0 jest: ~29.7.0 npm-run-all: ^4.1.5 peggy: 3.0.2 - prettier: ~2.8.7 + prettier: ~2.8.8 prettier-plugin-pegjs: ~0.5.4 rimraf: ^3.0.2 tldts: ~5.7.112 - ts-loader: ~9.4.2 - typedoc: ~0.24.1 + ts-loader: ~9.4.4 + typedoc: ~0.24.8 typescript: ~5.5.4 webpack: ~5.78.0 - webpack-cli: ~5.0.1 + webpack-cli: ~5.0.2 languageName: unknown linkType: soft @@ -9319,7 +9335,7 @@ __metadata: dependencies: "@axe-core/playwright": ^4.7.3 "@babel/core": ~7.22.20 - "@babel/eslint-parser": ~7.23.3 + "@babel/eslint-parser": ~7.23.10 "@babel/plugin-proposal-nullish-coalescing-operator": ~7.18.6 "@babel/plugin-proposal-optional-chaining": ~7.21.0 "@babel/preset-env": ~7.22.20 @@ -9330,7 +9346,7 @@ __metadata: "@bugsnag/plugin-react": ~7.19.0 "@faker-js/faker": ~8.0.2 "@google-cloud/storage": ^6.11.0 - "@kaciras/deasync": ^1.0.3 + "@kaciras/deasync": ^1.0.4 "@nivo/bar": 0.84.0 "@nivo/core": 0.84.0 "@nivo/heatmap": 0.84.0 @@ -9368,7 +9384,7 @@ __metadata: "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/jwt": "workspace:^" - "@rocket.chat/layout": ~0.31.26 + "@rocket.chat/layout": ~0.31.27 "@rocket.chat/license": "workspace:^" "@rocket.chat/livechat": "workspace:^" "@rocket.chat/log-format": "workspace:^" @@ -9414,87 +9430,87 @@ __metadata: "@storybook/testing-library": 0.0.13 "@tanstack/react-query": ^4.16.1 "@tanstack/react-query-devtools": ^4.19.1 - "@testing-library/react": ~16.0.0 + "@testing-library/react": ~16.0.1 "@testing-library/user-event": ~14.5.2 - "@types/adm-zip": ^0.5.3 + "@types/adm-zip": ^0.5.5 "@types/archiver": ^5.3.4 - "@types/bad-words": ^3.0.2 - "@types/bcrypt": ^5.0.1 - "@types/body-parser": ^1.19.4 - "@types/busboy": ^1.5.2 - "@types/chai": ~4.3.16 - "@types/chai-as-promised": ^7.1.7 - "@types/chai-datetime": 0.0.38 - "@types/chai-dom": 1.11.2 - "@types/chai-spies": ~1.0.5 - "@types/codemirror": ^5.60.12 - "@types/cookie": ^0.5.3 - "@types/cookie-parser": ^1.4.5 - "@types/cors": ^2.8.15 - "@types/cssom": ^0.4.2 + "@types/bad-words": ^3.0.3 + "@types/bcrypt": ^5.0.2 + "@types/body-parser": ^1.19.5 + "@types/busboy": ^1.5.4 + "@types/chai": ~4.3.19 + "@types/chai-as-promised": ^7.1.8 + "@types/chai-datetime": 0.0.39 + "@types/chai-dom": 1.11.3 + "@types/chai-spies": ~1.0.6 + "@types/codemirror": ^5.60.15 + "@types/cookie": ^0.5.4 + "@types/cookie-parser": ^1.4.7 + "@types/cors": ^2.8.17 + "@types/cssom": ^0.4.3 "@types/dompurify": ^2.3.3 - "@types/ejson": ^2.2.1 - "@types/express": ^4.17.20 + "@types/ejson": ^2.2.2 + "@types/express": ^4.17.21 "@types/express-rate-limit": ^5.1.3 - "@types/fibers": ^3.1.3 - "@types/google-libphonenumber": ^7.4.29 - "@types/gravatar": ^1.8.5 + "@types/fibers": ^3.1.4 + "@types/google-libphonenumber": ^7.4.30 + "@types/gravatar": ^1.8.6 "@types/he": ^1.1.2 - "@types/i18next-sprintf-postprocessor": ^0.2.2 - "@types/imap": ^0.8.39 - "@types/jest": ~29.5.12 + "@types/i18next-sprintf-postprocessor": ^0.2.3 + "@types/imap": ^0.8.40 + "@types/jest": ~29.5.13 "@types/jsdom": ^16.2.15 - "@types/jsdom-global": ^3.0.6 - "@types/jsrsasign": ^10.5.11 + "@types/jsdom-global": ^3.0.7 + "@types/jsrsasign": ^10.5.14 "@types/katex": ^0.14.0 - "@types/later": ^1.2.8 + "@types/later": ^1.2.9 "@types/ldapjs": ^2.2.5 - "@types/less": ~3.0.5 + "@types/less": ~3.0.6 "@types/lodash": ^4.14.200 "@types/lodash.clonedeep": ^4.5.9 - "@types/lodash.debounce": ^4.0.8 - "@types/lodash.get": ^4.4.8 - "@types/mailparser": ^3.4.3 + "@types/lodash.debounce": ^4.0.9 + "@types/lodash.get": ^4.4.9 + "@types/mailparser": ^3.4.4 "@types/marked": ^4.0.8 "@types/meteor": ^2.9.8 - "@types/meteor-collection-hooks": ^0.8.8 + "@types/meteor-collection-hooks": ^0.8.9 "@types/mkdirp": ^1.0.2 "@types/mocha": "github:whitecolor/mocha-types" "@types/moment-timezone": ^0.5.30 "@types/node": ^14.18.63 - "@types/node-gcm": ^1.0.3 - "@types/node-rsa": ^1.1.3 - "@types/nodemailer": ^6.4.13 - "@types/oauth2-server": ^3.0.15 - "@types/object-path": ^0.11.3 - "@types/parseurl": ^1.3.2 - "@types/prometheus-gc-stats": ^0.6.3 - "@types/proxy-from-env": ^1.0.3 - "@types/proxyquire": ^1.3.30 - "@types/psl": ^1.1.2 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 - "@types/rewire": ^2.5.29 + "@types/node-gcm": ^1.0.5 + "@types/node-rsa": ^1.1.4 + "@types/nodemailer": ^6.4.15 + "@types/oauth2-server": ^3.0.17 + "@types/object-path": ^0.11.4 + "@types/parseurl": ^1.3.3 + "@types/prometheus-gc-stats": ^0.6.4 + "@types/proxy-from-env": ^1.0.4 + "@types/proxyquire": ^1.3.31 + "@types/psl": ^1.1.3 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 + "@types/rewire": ^2.5.30 "@types/sanitize-html": ^2.9.3 "@types/semver": ^7.3.10 "@types/sharp": ^0.30.5 "@types/sinon": ^10.0.20 - "@types/speakeasy": ^2.0.9 - "@types/strict-uri-encode": ^2.0.1 + "@types/speakeasy": ^2.0.10 + "@types/strict-uri-encode": ^2.0.2 "@types/string-strip-html": ^5.0.1 - "@types/supertest": ^2.0.15 - "@types/supports-color": ~7.2.0 - "@types/textarea-caret": ^3.0.2 - "@types/ua-parser-js": ^0.7.38 - "@types/use-subscription": ^1.0.1 - "@types/use-sync-external-store": ^0.0.5 + "@types/supertest": ^2.0.16 + "@types/supports-color": ~7.2.1 + "@types/textarea-caret": ^3.0.3 + "@types/ua-parser-js": ^0.7.39 + "@types/use-subscription": ^1.0.2 + "@types/use-sync-external-store": ^0.0.6 "@types/uuid": ^8.3.4 - "@types/xml-crypto": ~1.4.4 - "@types/xml-encryption": ~1.2.3 + "@types/xml-crypto": ~1.4.6 + "@types/xml-encryption": ~1.2.4 "@typescript-eslint/eslint-plugin": ~5.60.1 "@typescript-eslint/parser": ~5.60.1 "@xmldom/xmldom": ^0.8.10 - adm-zip: 0.5.10 + adm-zip: 0.5.16 ajv: ^8.11.0 ajv-formats: ~2.1.1 apn: 2.2.0 @@ -9509,19 +9525,19 @@ __metadata: babel-plugin-istanbul: ^6.1.1 bad-words: ^3.0.4 bcrypt: ^5.0.1 - body-parser: 1.20.2 + body-parser: 1.20.3 bson: ^4.6.4 busboy: ^1.6.0 bytebuffer: 5.0.1 chai: ^4.3.10 - chai-as-promised: ^7.1.1 - chai-datetime: ^1.8.0 + chai-as-promised: ^7.1.2 + chai-datetime: ^1.8.1 chai-dom: ^1.11.0 chai-spies: ~1.0.0 chalk: ^4.0.0 change-case: ^4.1.2 chart.js: ^3.8.0 - codemirror: ^5.65.15 + codemirror: ^5.65.17 colorette: ^2.0.20 colors: ^1.4.0 connect: ^3.7.0 @@ -9530,12 +9546,12 @@ __metadata: cors: ^2.8.5 cron: ~1.8.2 cross-env: ^7.0.3 - css-vars-ponyfill: ^2.4.8 + css-vars-ponyfill: ^2.4.9 csv-parse: ^5.2.0 date-fns: ^2.28.0 date.js: ~0.3.3 debug: ~4.1.1 - docker-compose: ^0.24.3 + docker-compose: ^0.24.8 dompurify: ^2.3.8 ejson: ^2.2.3 emailreplyparser: ^0.0.5 @@ -9550,7 +9566,7 @@ __metadata: eslint-plugin-playwright: ~0.15.3 eslint-plugin-prettier: ~4.2.1 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-testing-library: ~6.2.2 eslint-plugin-you-dont-need-lodash-underscore: ~6.12.0 eventemitter3: ^4.0.7 @@ -9565,7 +9581,7 @@ __metadata: filenamify: ^4.3.0 filesize: 9.0.11 generate-password: ^1.7.1 - google-libphonenumber: ^3.2.33 + google-libphonenumber: ^3.2.38 gravatar: ^1.8.2 he: ^1.2.0 highlight.js: ^11.6.0 @@ -9587,7 +9603,7 @@ __metadata: jsdom-global: ^3.0.2 jsrsasign: ^10.5.24 juice: ^8.0.0 - katex: ~0.16.9 + katex: ~0.16.11 ldap-escape: ^2.0.6 ldapjs: ^2.3.3 limax: ^3.0.0 @@ -9598,14 +9614,14 @@ __metadata: mailparser: ^3.4.0 marked: ^4.2.5 mem: ^8.1.1 - meteor-node-stubs: ^1.2.5 + meteor-node-stubs: ^1.2.10 mime-db: ^1.52.0 mime-type: ^4.0.0 mkdirp: ^1.0.4 mocha: ^9.2.2 - moleculer: ^0.14.31 + moleculer: ^0.14.34 moment: ^2.29.4 - moment-timezone: ^0.5.43 + moment-timezone: ^0.5.45 mongo-message-queue: ^1.0.0 mongodb: ^4.17.2 nats: ^2.6.1 @@ -9624,8 +9640,8 @@ __metadata: pdfjs-dist: ^2.13.216 pino: ^8.15.0 pino-pretty: ^7.6.1 - playwright-qase-reporter: ^1.2.1 - postcss: ~8.4.31 + playwright-qase-reporter: ^1.2.2 + postcss: ~8.4.45 postcss-custom-properties: ^11.0.0 postcss-easy-import: ^3.0.0 postcss-load-config: ^3.1.4 @@ -9642,10 +9658,10 @@ __metadata: query-string: ^7.1.3 queue-fifo: ^0.2.6 rc-scrollbars: ^1.1.6 - re-resizable: ^6.9.9 + re-resizable: ^6.9.18 react: ~17.0.2 react-aria: ~3.23.1 - react-docgen-typescript-plugin: ^1.0.5 + react-docgen-typescript-plugin: ^1.0.8 react-dom: ~17.0.2 react-error-boundary: ^3.1.4 react-hook-form: ~7.45.4 @@ -9663,7 +9679,7 @@ __metadata: sodium-plus: ^0.9.0 source-map: ^0.7.4 speakeasy: ^2.0.0 - stream-buffers: ^3.0.2 + stream-buffers: ^3.0.3 strict-uri-encode: ^2.0.0 string-strip-html: ^7.0.3 stylelint: ^14.9.1 @@ -9676,21 +9692,21 @@ __metadata: template-file: ^6.0.1 textarea-caret: ^3.1.0 tinykeys: ^1.4.0 - ts-node: ^10.9.1 + ts-node: ^10.9.2 turndown: ^7.1.2 twilio: ^3.76.1 twit: ^2.2.11 typescript: ~5.5.4 typia: ~6.9.0 - ua-parser-js: ^1.0.37 - underscore: ^1.13.6 + ua-parser-js: ^1.0.38 + underscore: ^1.13.7 universal-perf-hooks: ^1.0.1 url-polyfill: ^1.1.12 use-subscription: ~1.6.0 - use-sync-external-store: ^1.2.0 + use-sync-external-store: ^1.2.2 uuid: ^8.3.2 vm2: ^3.9.19 - webdav: ^4.11.3 + webdav: ^4.11.4 xml-crypto: ~3.1.0 xml-encryption: ~3.0.2 xml2js: ~0.5.0 @@ -9708,7 +9724,7 @@ __metadata: "@rocket.chat/ui-contexts": "workspace:*" "@storybook/react": ~6.5.16 "@tanstack/react-query": ^4.16.1 - "@types/use-sync-external-store": ^0.0.5 + "@types/use-sync-external-store": ^0.0.6 eslint: ~8.45.0 i18next: ~23.14.0 react: ~17.0.2 @@ -9726,7 +9742,7 @@ __metadata: resolution: "@rocket.chat/model-typings@workspace:packages/model-typings" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@types/node-rsa": ^1.1.3 + "@types/node-rsa": ^1.1.4 eslint: ~8.45.0 mongodb: ^4.17.2 typescript: ~5.5.4 @@ -9739,7 +9755,7 @@ __metadata: dependencies: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/model-typings": "workspace:~" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 eslint: ~8.45.0 jest: ^29.7.0 typescript: ~5.5.4 @@ -9771,7 +9787,7 @@ __metadata: "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/tools": "workspace:^" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 "@types/node": ^14.18.63 date-fns: ^2.28.0 ejson: ^2.2.3 @@ -9781,7 +9797,7 @@ __metadata: fibers: ^5.0.3 jest: ~29.7.0 mem: ^8.1.1 - moment-timezone: ^0.5.43 + moment-timezone: ^0.5.45 mongo-message-queue: ^1.0.0 mongodb: ^4.17.2 pino: ^8.15.0 @@ -9804,25 +9820,25 @@ __metadata: "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" "@rocket.chat/tools": "workspace:^" - "@types/gc-stats": ^1.4.2 + "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 ejson: ^2.2.3 emoji-toolkit: ^7.0.1 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 mem: ^8.1.1 - moleculer: ^0.14.31 - moment-timezone: ^0.5.43 + moleculer: ^0.14.34 + moment-timezone: ^0.5.45 mongo-message-queue: ^1.0.0 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 polka: ^0.5.2 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 languageName: unknown linkType: soft @@ -9853,7 +9869,7 @@ __metadata: resolution: "@rocket.chat/password-policies@workspace:packages/password-policies" dependencies: "@rocket.chat/jest-presets": "workspace:~" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 eslint: ~8.45.0 jest: ~29.7.0 typescript: ~5.5.4 @@ -9865,7 +9881,7 @@ __metadata: resolution: "@rocket.chat/patch-injection@workspace:packages/patch-injection" dependencies: "@rocket.chat/jest-presets": "workspace:~" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 eslint: ~8.45.0 jest: ~29.7.0 typescript: ~5.5.4 @@ -9882,17 +9898,17 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@storybook/addon-essentials": ~6.5.16 "@storybook/react": ~6.5.16 - "@testing-library/react": ~16.0.0 - "@types/emojione": ^2.2.8 - "@types/jest": ~29.5.12 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 + "@testing-library/react": ~16.0.1 + "@types/emojione": ^2.2.9 + "@types/jest": ~29.5.13 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 emoji-assets: ^7.0.1 emoji-toolkit: ^7.0.1 eslint: ~8.45.0 jest: ~29.7.0 moment: ^2.29.4 - moment-timezone: ^0.5.43 + moment-timezone: ^0.5.45 react: ~18.3.1 react-dom: ~18.3.1 typescript: ~5.5.4 @@ -9905,11 +9921,11 @@ __metadata: dependencies: "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/prettier-config": ~0.31.25 - "@types/node": ~14.18.42 + "@types/node": ~14.18.63 eslint: ~8.45.0 npm-run-all: ^4.1.5 peggy: 3.0.2 - prettier: ~2.8.7 + prettier: ~2.8.8 rimraf: ^3.0.2 typescript: ~5.5.4 webpack: ~5.78.0 @@ -9940,22 +9956,22 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 - "@types/gc-stats": ^1.4.2 + "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 ejson: ^2.2.3 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 mem: ^8.1.1 - moleculer: ^0.14.31 + moleculer: ^0.14.34 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 polka: ^0.5.2 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 languageName: unknown linkType: soft @@ -10004,25 +10020,25 @@ __metadata: "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" - "@types/gc-stats": ^1.4.2 + "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 ejson: ^2.2.3 emoji-toolkit: ^7.0.1 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 mem: ^8.1.1 - moleculer: ^0.14.31 - moment-timezone: ^0.5.43 + moleculer: ^0.14.34 + moment-timezone: ^0.5.45 mongo-message-queue: ^1.0.0 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 polka: ^0.5.2 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 languageName: unknown linkType: soft @@ -10052,7 +10068,7 @@ __metadata: "@actions/github": ^5.1.1 "@octokit/plugin-throttling": ^6.0.0 "@rocket.chat/eslint-config": "workspace:^" - "@types/node": ^16.18.60 + "@types/node": ^16.18.108 eslint: ~8.45.0 mdast-util-to-string: 2.0.0 remark-parse: 9.0.0 @@ -10072,7 +10088,7 @@ __metadata: "@types/node": ^14.18.63 dataloader: ^1.4.0 eslint: ~8.45.0 - node-fetch: ^2 + node-fetch: ^2.7.0 typescript: ~5.5.4 languageName: unknown linkType: soft @@ -10086,7 +10102,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 ajv: ^8.11.0 ajv-formats: ^2.1.1 eslint: ~8.45.0 @@ -10123,7 +10139,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/server-fetch@workspace:packages/server-fetch" dependencies: - "@types/proxy-from-env": ^1.0.3 + "@types/proxy-from-env": ^1.0.4 eslint: ~8.45.0 http-proxy-agent: ^5.0.0 https-proxy-agent: ^5.0.1 @@ -10162,23 +10178,23 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 - "@types/bcrypt": ^5.0.1 - "@types/gc-stats": ^1.4.2 + "@types/bcrypt": ^5.0.2 + "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 - "@types/polka": ^0.5.6 + "@types/polka": ^0.5.7 ejson: ^2.2.3 eslint: ~8.45.0 event-loop-stats: ^1.4.1 eventemitter3: ^4.0.7 fibers: ^5.0.3 - gc-stats: ^1.4.0 + gc-stats: ^1.4.1 mem: ^8.1.1 - moleculer: ^0.14.31 + moleculer: ^0.14.34 mongodb: ^4.17.2 nats: ^2.4.0 pino: ^8.15.0 polka: ^0.5.2 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 languageName: unknown linkType: soft @@ -10215,27 +10231,26 @@ __metadata: resolution: "@rocket.chat/tools@workspace:packages/tools" dependencies: "@rocket.chat/jest-presets": "workspace:~" - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 eslint: ~8.45.0 jest: ~29.7.0 - moment-timezone: ^0.5.43 + moment-timezone: ^0.5.45 typescript: ~5.5.4 languageName: unknown linkType: soft -"@rocket.chat/ui-avatar@workspace:^, @rocket.chat/ui-avatar@workspace:packages/ui-avatar": +"@rocket.chat/ui-avatar@workspace:^, @rocket.chat/ui-avatar@workspace:packages/ui-avatar, @rocket.chat/ui-avatar@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": ~7.22.20 "@rocket.chat/fuselage": ^0.59.0 "@rocket.chat/ui-contexts": "workspace:^" - "@types/babel__core": ~7.20.3 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 eslint: ~8.45.0 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-storybook: ~0.6.15 eslint-plugin-testing-library: ~5.11.1 react: ^17.0.2 @@ -10259,6 +10274,7 @@ __metadata: "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/mock-providers": "workspace:^" + "@rocket.chat/ui-avatar": "workspace:~" "@rocket.chat/ui-contexts": "workspace:~" "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10270,15 +10286,14 @@ __metadata: "@storybook/manager-webpack4": ~6.5.16 "@storybook/react": ~6.5.16 "@storybook/testing-library": ~0.0.13 - "@testing-library/react": ~16.0.0 - "@types/babel__core": ~7.20.3 - "@types/jest": ~29.5.12 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 + "@testing-library/react": ~16.0.1 + "@types/jest": ~29.5.13 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 eslint: ~8.45.0 eslint-plugin-anti-trojan-source: ~1.1.1 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-storybook: ~0.6.15 eslint-plugin-testing-library: ~5.11.1 jest: ~29.7.0 @@ -10293,6 +10308,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" + "@rocket.chat/ui-avatar": "*" "@rocket.chat/ui-contexts": "*" react: "*" react-i18next: "*" @@ -10315,15 +10331,14 @@ __metadata: "@storybook/manager-webpack4": ~6.5.16 "@storybook/react": ~6.5.16 "@storybook/testing-library": ~0.0.13 - "@types/babel__core": ~7.20.3 - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 eslint: ~8.45.0 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-storybook: ~0.6.15 react: ~17.0.2 - react-docgen-typescript-plugin: ~1.0.5 + react-docgen-typescript-plugin: ~1.0.8 react-dom: ~17.0.2 typescript: ~5.5.4 peerDependencies: @@ -10346,15 +10361,15 @@ __metadata: "@rocket.chat/i18n": "workspace:~" "@rocket.chat/password-policies": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" - "@types/react": ~17.0.69 - "@types/react-dom": ~17.0.22 - "@types/use-sync-external-store": ^0.0.5 + "@types/react": ~17.0.80 + "@types/react-dom": ~17.0.25 + "@types/use-sync-external-store": ^0.0.6 eslint: ~8.45.0 - eslint-plugin-react-hooks: ^4.6.0 + eslint-plugin-react-hooks: ^4.6.2 mongodb: ^4.17.2 react: ~17.0.2 typescript: ~5.5.4 - use-sync-external-store: ^1.2.0 + use-sync-external-store: ^1.2.2 peerDependencies: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:^" @@ -10371,23 +10386,23 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-kit@workspace:packages/ui-kit" dependencies: - "@babel/core": ~7.21.4 - "@babel/eslint-parser": ~7.23.3 + "@babel/core": ~7.21.8 + "@babel/eslint-parser": ~7.23.10 "@babel/plugin-transform-runtime": ~7.21.4 - "@babel/preset-env": ~7.21.4 + "@babel/preset-env": ~7.21.5 "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" - "@types/jest": ~29.5.12 - babel-loader: ~9.1.2 + "@types/jest": ~29.5.13 + babel-loader: ~9.1.3 eslint: ~8.45.0 jest: ~29.7.0 npm-run-all: ~4.1.5 prettier: ~2.8.8 rimraf: ~3.0.2 - ts-jest: ~29.1.1 - ts-loader: ~9.4.2 - ts-node: ~10.9.1 + ts-jest: ~29.1.5 + ts-loader: ~9.4.4 + ts-node: ~10.9.2 ts-patch: ~3.2.1 typescript: ~5.5.4 typia: ~6.9.0 @@ -10405,14 +10420,14 @@ __metadata: "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/ui-contexts": "workspace:~" - "@types/react": ~17.0.69 + "@types/react": ~17.0.80 eslint: ~8.45.0 eslint-plugin-anti-trojan-source: ~1.1.1 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-testing-library: ^5.11.1 react: ~17.0.2 - react-docgen-typescript-plugin: ~1.0.5 + react-docgen-typescript-plugin: ~1.0.8 typescript: ~5.5.4 peerDependencies: "@rocket.chat/css-in-js": "*" @@ -10447,16 +10462,15 @@ __metadata: "@storybook/react": ~6.5.16 "@storybook/testing-library": ~0.0.13 "@storybook/testing-react": ~1.3.0 - "@types/babel__core": ~7.20.3 - "@types/jest": ~29.5.12 + "@types/jest": ~29.5.13 "@types/jest-axe": ~3.5.9 eslint: ~8.45.0 eslint-plugin-react: ~7.32.2 - eslint-plugin-react-hooks: ~4.6.0 + eslint-plugin-react-hooks: ~4.6.2 eslint-plugin-storybook: ~0.6.15 jest: ~29.7.0 jest-axe: ~9.0.0 - react-docgen-typescript-plugin: ~1.0.5 + react-docgen-typescript-plugin: ~1.0.8 typescript: ~5.5.4 peerDependencies: "@rocket.chat/css-in-js": "*" @@ -10491,17 +10505,17 @@ __metadata: "@rocket.chat/styled": ~0.31.25 "@rocket.chat/ui-avatar": "workspace:^" "@rocket.chat/ui-contexts": "workspace:~" - "@types/react": ~17.0.69 - "@types/react-beautiful-dnd": ^13.1.6 - "@types/react-dom": ~17.0.22 - "@types/use-subscription": ^1.0.1 + "@types/react": ~17.0.80 + "@types/react-beautiful-dnd": ^13.1.8 + "@types/react-dom": ~17.0.25 + "@types/use-subscription": ^1.0.2 "@typescript-eslint/eslint-plugin": ~5.60.1 "@typescript-eslint/parser": ~5.60.1 "@vitejs/plugin-react": ^4.0.0 codemirror: ^6.0.1 eslint: ~8.45.0 - eslint-plugin-react-hooks: ^4.6.0 - eslint-plugin-react-refresh: ^0.4.4 + eslint-plugin-react-hooks: ^4.6.2 + eslint-plugin-react-refresh: ^0.4.11 eslint4b-prebuilt: ^6.7.2 moment: ^2.29.4 rc-scrollbars: ^1.1.6 @@ -10513,7 +10527,7 @@ __metadata: react-virtuoso: ^4.7.1 reactflow: ^11.7.2 typescript: ~5.5.4 - use-subscription: ^1.8.0 + use-subscription: ^1.8.2 vite: ^4.3.9 languageName: unknown linkType: soft @@ -10527,7 +10541,7 @@ __metadata: "@babel/preset-react": ~7.22.15 "@babel/preset-typescript": ~7.22.15 "@rocket.chat/i18n": "workspace:~" - "@rocket.chat/layout": ~0.31.26 + "@rocket.chat/layout": ~0.31.27 "@rocket.chat/mock-providers": "workspace:~" "@rocket.chat/tools": "workspace:~" "@rocket.chat/ui-client": "workspace:^" @@ -10540,14 +10554,14 @@ __metadata: "@storybook/react": ~6.5.16 "@storybook/testing-library": ^0.2.2 "@tanstack/react-query": ^4.16.1 - "@testing-library/react": ~16.0.0 - "@types/react": ~17.0.69 + "@testing-library/react": ~16.0.1 + "@types/react": ~17.0.80 babel-loader: ~8.3.0 eslint: ~8.45.0 react: ~17.0.2 react-hook-form: ~7.45.4 react-i18next: ~15.0.1 - storybook-dark-mode: ~3.0.1 + storybook-dark-mode: ~3.0.3 typescript: ~5.5.4 peerDependencies: "@rocket.chat/layout": "*" @@ -11278,24 +11292,6 @@ __metadata: languageName: node linkType: hard -"@storybook/api@npm:^7.0.0": - version: 7.3.2 - resolution: "@storybook/api@npm:7.3.2" - dependencies: - "@storybook/client-logger": 7.3.2 - "@storybook/manager-api": 7.3.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: dfe5f976256fc74c82548ca35f7052b82ca87a21d44e8554c0913ecc8f5d0e0d317a57b0308057473c3b8efa95428590d4350c9c4d1ef7f7c02ee8c7c2d2e8ee - languageName: node - linkType: hard - "@storybook/builder-webpack4@npm:6.5.16, @storybook/builder-webpack4@npm:~6.5.16": version: 6.5.16 resolution: "@storybook/builder-webpack4@npm:6.5.16" @@ -11462,6 +11458,20 @@ __metadata: languageName: node linkType: hard +"@storybook/channels@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/channels@npm:7.6.20" + dependencies: + "@storybook/client-logger": 7.6.20 + "@storybook/core-events": 7.6.20 + "@storybook/global": ^5.0.0 + qs: ^6.10.0 + telejson: ^7.2.0 + tiny-invariant: ^1.3.1 + checksum: e600949b77b8ae2c865eab8e2f4022893843932c76f9f49f2ef6d8a8f62114f979d38fe93fed2d8899fa892bb1dcd353c81e292515a7bd8fbc944629939574b0 + languageName: node + linkType: hard + "@storybook/client-api@npm:6.5.16": version: 6.5.16 resolution: "@storybook/client-api@npm:6.5.16" @@ -11512,6 +11522,15 @@ __metadata: languageName: node linkType: hard +"@storybook/client-logger@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/client-logger@npm:7.6.20" + dependencies: + "@storybook/global": ^5.0.0 + checksum: 98bf603df918a74bc5b34f344ba70356d8e2b889d8a5cbfb7a03c23dcf409029800319e5d3e0ff7e95545a2d4a989a53c2097eb7a36de808d287d466d4d92573 + languageName: node + linkType: hard + "@storybook/components@npm:6.5.16": version: 6.5.16 resolution: "@storybook/components@npm:6.5.16" @@ -11668,6 +11687,15 @@ __metadata: languageName: node linkType: hard +"@storybook/core-events@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/core-events@npm:7.6.20" + dependencies: + ts-dedent: ^2.0.0 + checksum: 284f4df326200dc0bdfc74472dccab3bbed38cf8515baebe467830b562f9ace06fb6e5640b155b4ae462288b9f3257233c6b5212fcb9b2d3024f9e4d08d28457 + languageName: node + linkType: hard + "@storybook/core-server@npm:6.5.16": version: 6.5.16 resolution: "@storybook/core-server@npm:6.5.16" @@ -11815,6 +11843,15 @@ __metadata: languageName: node linkType: hard +"@storybook/csf@npm:^0.1.2": + version: 0.1.11 + resolution: "@storybook/csf@npm:0.1.11" + dependencies: + type-fest: ^2.19.0 + checksum: ba2a265f62ad82a2853b069f77e974efe31bed263a640ca1dd8e6d7e194022018a67ad4a2587ae928f33ae45aaf6ffedd5925ba3fcf3fe5b7996667a918e22eb + languageName: node + linkType: hard + "@storybook/docs-tools@npm:6.5.16": version: 6.5.16 resolution: "@storybook/docs-tools@npm:6.5.16" @@ -11886,6 +11923,28 @@ __metadata: languageName: node linkType: hard +"@storybook/manager-api@npm:^7.0.0": + version: 7.6.20 + resolution: "@storybook/manager-api@npm:7.6.20" + dependencies: + "@storybook/channels": 7.6.20 + "@storybook/client-logger": 7.6.20 + "@storybook/core-events": 7.6.20 + "@storybook/csf": ^0.1.2 + "@storybook/global": ^5.0.0 + "@storybook/router": 7.6.20 + "@storybook/theming": 7.6.20 + "@storybook/types": 7.6.20 + dequal: ^2.0.2 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + store2: ^2.14.2 + telejson: ^7.2.0 + ts-dedent: ^2.0.0 + checksum: 01a35e757b0e673570a6868fa64b9a3a258011b6c16afe6e0164ae8406896eeaf53d9a069db6862cbd4b10b64546f0c78ed96872c036a02869b2f530603e9dec + languageName: node + linkType: hard + "@storybook/manager-webpack4@npm:6.5.16, @storybook/manager-webpack4@npm:~6.5.16": version: 6.5.16 resolution: "@storybook/manager-webpack4@npm:6.5.16" @@ -12233,6 +12292,17 @@ __metadata: languageName: node linkType: hard +"@storybook/router@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/router@npm:7.6.20" + dependencies: + "@storybook/client-logger": 7.6.20 + memoizerific: ^1.11.3 + qs: ^6.10.0 + checksum: 67af15f4144e674dcd957fc8ae2ed6e7cbd3845c8fb1343760ec67bebf04eeb54b7088db701208c976026641373dc2287f6908dbc7e3d96da9a1c910054ba942 + languageName: node + linkType: hard + "@storybook/semver@npm:^7.3.2": version: 7.3.2 resolution: "@storybook/semver@npm:7.3.2" @@ -12381,6 +12451,21 @@ __metadata: languageName: node linkType: hard +"@storybook/theming@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/theming@npm:7.6.20" + dependencies: + "@emotion/use-insertion-effect-with-fallbacks": ^1.0.0 + "@storybook/client-logger": 7.6.20 + "@storybook/global": ^5.0.0 + memoizerific: ^1.11.3 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 2c3cbac759d300fb471acf4e01514f7728c48533c6e24c37b0351d9f0fcc9240dbdcf50067a81c3ff73b27a1da4227debb55a48ab2920368099871d1f49252ea + languageName: node + linkType: hard + "@storybook/types@npm:7.3.2": version: 7.3.2 resolution: "@storybook/types@npm:7.3.2" @@ -12393,6 +12478,18 @@ __metadata: languageName: node linkType: hard +"@storybook/types@npm:7.6.20": + version: 7.6.20 + resolution: "@storybook/types@npm:7.6.20" + dependencies: + "@storybook/channels": 7.6.20 + "@types/babel__core": ^7.0.0 + "@types/express": ^4.7.0 + file-system-cache: 2.3.0 + checksum: 1e0ae196c63ace6a9a0f06a42c7294cfc840665d1ff7ae6d9fd2466ed3d78387672951aa7a9064a719384938c57d8eb25c8089657710d95c546344fbc28d8df6 + languageName: node + linkType: hard + "@storybook/ui@npm:6.5.16": version: 6.5.16 resolution: "@storybook/ui@npm:6.5.16" @@ -12418,90 +12515,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-darwin-arm64@npm:1.7.23" +"@swc/core-darwin-arm64@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-darwin-arm64@npm:1.7.26" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-darwin-x64@npm:1.7.23" +"@swc/core-darwin-x64@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-darwin-x64@npm:1.7.26" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.23" +"@swc/core-linux-arm-gnueabihf@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.7.26" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-linux-arm64-gnu@npm:1.7.23" +"@swc/core-linux-arm64-gnu@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-linux-arm64-gnu@npm:1.7.26" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-linux-arm64-musl@npm:1.7.23" +"@swc/core-linux-arm64-musl@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-linux-arm64-musl@npm:1.7.26" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-linux-x64-gnu@npm:1.7.23" +"@swc/core-linux-x64-gnu@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-linux-x64-gnu@npm:1.7.26" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-linux-x64-musl@npm:1.7.23" +"@swc/core-linux-x64-musl@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-linux-x64-musl@npm:1.7.26" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-win32-arm64-msvc@npm:1.7.23" +"@swc/core-win32-arm64-msvc@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-win32-arm64-msvc@npm:1.7.26" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-win32-ia32-msvc@npm:1.7.23" +"@swc/core-win32-ia32-msvc@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-win32-ia32-msvc@npm:1.7.26" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.7.23": - version: 1.7.23 - resolution: "@swc/core-win32-x64-msvc@npm:1.7.23" +"@swc/core-win32-x64-msvc@npm:1.7.26": + version: 1.7.26 + resolution: "@swc/core-win32-x64-msvc@npm:1.7.26" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@swc/core@npm:~1.7.23": - version: 1.7.23 - resolution: "@swc/core@npm:1.7.23" +"@swc/core@npm:~1.7.26": + version: 1.7.26 + resolution: "@swc/core@npm:1.7.26" dependencies: - "@swc/core-darwin-arm64": 1.7.23 - "@swc/core-darwin-x64": 1.7.23 - "@swc/core-linux-arm-gnueabihf": 1.7.23 - "@swc/core-linux-arm64-gnu": 1.7.23 - "@swc/core-linux-arm64-musl": 1.7.23 - "@swc/core-linux-x64-gnu": 1.7.23 - "@swc/core-linux-x64-musl": 1.7.23 - "@swc/core-win32-arm64-msvc": 1.7.23 - "@swc/core-win32-ia32-msvc": 1.7.23 - "@swc/core-win32-x64-msvc": 1.7.23 + "@swc/core-darwin-arm64": 1.7.26 + "@swc/core-darwin-x64": 1.7.26 + "@swc/core-linux-arm-gnueabihf": 1.7.26 + "@swc/core-linux-arm64-gnu": 1.7.26 + "@swc/core-linux-arm64-musl": 1.7.26 + "@swc/core-linux-x64-gnu": 1.7.26 + "@swc/core-linux-x64-musl": 1.7.26 + "@swc/core-win32-arm64-msvc": 1.7.26 + "@swc/core-win32-ia32-msvc": 1.7.26 + "@swc/core-win32-x64-msvc": 1.7.26 "@swc/counter": ^0.1.3 "@swc/types": ^0.1.12 peerDependencies: @@ -12530,7 +12627,7 @@ __metadata: peerDependenciesMeta: "@swc/helpers": optional: true - checksum: a21e9a67d305ad9b49b6def9f9698b374db6c15b2ded5e5cf29390221181fc6b91bfa317b53fd41d9cca989d85b79bbbca5ab27635df1e0232cad4681f618fa5 + checksum: 8249f2af001f2b5b312ccace4e3a8575bf286bb0f67f029eb58ef8b144f73e37f766800859161ef87c0993e586055238c000ab17dddf9d3760edcfa63ffd22e6 languageName: node linkType: hard @@ -12679,9 +12776,9 @@ __metadata: languageName: node linkType: hard -"@testing-library/react@npm:~16.0.0": - version: 16.0.0 - resolution: "@testing-library/react@npm:16.0.0" +"@testing-library/react@npm:~16.0.1": + version: 16.0.1 + resolution: "@testing-library/react@npm:16.0.1" dependencies: "@babel/runtime": ^7.12.5 peerDependencies: @@ -12695,7 +12792,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 45a35f0b5f34b5a7f4dcefdd3f1d202d5421692e5cc7a491c9bc71e6ed9dd5872a182b80b4dfefb4a56d9c1df35e50f6fa2917bcf657cc26b4bc0d2259df0027 + checksum: 1837db473ea018cf2b5d0cbfffb7a30d0d759e5a7f23aad431441c77bcc3d2533250cd003a61878fd908267df47404cedcb5914f12d79e413002c659652b37fd languageName: node linkType: hard @@ -12775,12 +12872,12 @@ __metadata: languageName: node linkType: hard -"@types/adm-zip@npm:^0.5.3": - version: 0.5.3 - resolution: "@types/adm-zip@npm:0.5.3" +"@types/adm-zip@npm:^0.5.5": + version: 0.5.5 + resolution: "@types/adm-zip@npm:0.5.5" dependencies: "@types/node": "*" - checksum: 995e21441cc6fe180f12ebf4e722bc6dbde0f9c765e57353018a0969a27cf15542c3f5451bfa1a82b958d5ed5e371fb3cbe55f934076dae22d8bf6a259a536bf + checksum: 808c25b8a1c2e1c594cf9b1514e7953105cf96e19e38aa7dc109ff2537bda7345b950ef1f4e54a6e824e5503e29d24b0ff6d0aa1ff9bd4afb79ef0ef2df9ebab languageName: node linkType: hard @@ -12800,7 +12897,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.3, @types/babel__core@npm:~7.20.3": +"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14": version: 7.20.3 resolution: "@types/babel__core@npm:7.20.3" dependencies: @@ -12822,13 +12919,6 @@ __metadata: languageName: node linkType: hard -"@types/babel__preset-env@npm:^7.9.4": - version: 7.9.4 - resolution: "@types/babel__preset-env@npm:7.9.4" - checksum: a4580b541d4fe7bdf9ecb9695cb90827f96c75e5c807e6fcf6cbca3beeb9106019dc322b2d08de03025d57c6f3f1431d80eb3ea6d9416e90bc8ee15460efa27d - languageName: node - linkType: hard - "@types/babel__template@npm:*": version: 7.4.1 resolution: "@types/babel__template@npm:7.4.1" @@ -12848,23 +12938,23 @@ __metadata: languageName: node linkType: hard -"@types/bad-words@npm:^3.0.2": - version: 3.0.2 - resolution: "@types/bad-words@npm:3.0.2" - checksum: aa40d10364b73deb5617c0b53d5a782841ddd173e6cc68bc41d48c2c95c63ac81d23e2cabeb3d686cb7d655512353ac1866135203305b820d4c60859635ba9d3 +"@types/bad-words@npm:^3.0.3": + version: 3.0.3 + resolution: "@types/bad-words@npm:3.0.3" + checksum: 220729e6888b523477e3b1ca431b2fd01d816e9dbdd309c8a68b7a1a64ce02ed13ec44fe83bf83872f559d19b9180d4103b5f4179b7d4b1dd497306a46cb3c1d languageName: node linkType: hard -"@types/bcrypt@npm:^5.0.1": - version: 5.0.1 - resolution: "@types/bcrypt@npm:5.0.1" +"@types/bcrypt@npm:^5.0.2": + version: 5.0.2 + resolution: "@types/bcrypt@npm:5.0.2" dependencies: "@types/node": "*" - checksum: 2419ad2e7601b2c306ce2bd8bd8d911770f1abdea780efd4e61e9329b757e47c80fbd73fc52a925a371de47247ce92f04d11bdf24b8b44754ea0ba18d06d2202 + checksum: b1f97532ffe6079cb57a464f28b5b37a30bc9620f43469e1f27ab9c979c8a114be5b667e7b115a5556fd5be463b65968da9bb32573c6faf74fecf6e565d8974b languageName: node linkType: hard -"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.4": +"@types/body-parser@npm:*": version: 1.19.4 resolution: "@types/body-parser@npm:1.19.4" dependencies: @@ -12874,6 +12964,16 @@ __metadata: languageName: node linkType: hard +"@types/body-parser@npm:^1.19.5": + version: 1.19.5 + resolution: "@types/body-parser@npm:1.19.5" + dependencies: + "@types/connect": "*" + "@types/node": "*" + checksum: 1e251118c4b2f61029cc43b0dc028495f2d1957fe8ee49a707fb940f86a9bd2f9754230805598278fe99958b49e9b7e66eec8ef6a50ab5c1f6b93e1ba2aaba82 + languageName: node + linkType: hard + "@types/bonjour@npm:^3.5.9": version: 3.5.10 resolution: "@types/bonjour@npm:3.5.10" @@ -12883,58 +12983,65 @@ __metadata: languageName: node linkType: hard -"@types/busboy@npm:^1.5.2": - version: 1.5.2 - resolution: "@types/busboy@npm:1.5.2" +"@types/busboy@npm:^1.5.4": + version: 1.5.4 + resolution: "@types/busboy@npm:1.5.4" dependencies: "@types/node": "*" - checksum: d2cfa334e06ce0fbeb31bdac7b4ba051b85e508f4f46d016e55488a717c626e19285f0e2fae40d0183d0f676dd162ab9216613799f83d40430e1996fba4bfb2f + checksum: 56444ed1223d5c8f6340128561c25bf7d6567494f507d386241de2ed6e22f9a69de7f536d71502c2f0897f81be7e8253455522ddd791db7e3eb769db7eae9e92 languageName: node linkType: hard -"@types/chai-as-promised@npm:^7.1.7": - version: 7.1.7 - resolution: "@types/chai-as-promised@npm:7.1.7" +"@types/chai-as-promised@npm:^7.1.8": + version: 7.1.8 + resolution: "@types/chai-as-promised@npm:7.1.8" dependencies: "@types/chai": "*" - checksum: 59199afbd91289588648e263d7f32f7d72fa9c0075f3c17b1e760e10fdc1a310c2170a392b0d17d96cfff2c51daca72839eed6d80142f9230c9784b8e08ba676 + checksum: f0e5eab451b91bc1e289ed89519faf6591932e8a28d2ec9bbe95826eb73d28fe43713633e0c18706f3baa560a7d97e7c7c20dc53ce639e5d75bac46b2a50bf21 languageName: node linkType: hard -"@types/chai-datetime@npm:0.0.38": - version: 0.0.38 - resolution: "@types/chai-datetime@npm:0.0.38" +"@types/chai-datetime@npm:0.0.39": + version: 0.0.39 + resolution: "@types/chai-datetime@npm:0.0.39" dependencies: "@types/chai": "*" - checksum: 2c0269e91a282ef71fe5603a94b6df4c3b784e3ded25f380ed91c0d91c26d08ac33da6bde903bc9bddfec4636455378c26810947a989ef87fe37078213e350ca + checksum: fc5266f839f7005b34d04d4e3f67b232ec86eb50291cf4769f4875bc81a21afd8c9a51547da71f7903e2124379019b83bfc400a1b6315c719888dedb678c760b languageName: node linkType: hard -"@types/chai-dom@npm:1.11.2": - version: 1.11.2 - resolution: "@types/chai-dom@npm:1.11.2" +"@types/chai-dom@npm:1.11.3": + version: 1.11.3 + resolution: "@types/chai-dom@npm:1.11.3" dependencies: "@types/chai": "*" - checksum: d59a90bf2b497797178d3a5bd17f242caaf6ee098563142fdc765a6c12786f94bc0a9b8e4023de37ef8c803198ce9562823651610b02c80f1e21a29f2ece80fd + checksum: 274afd91062b54fb56c95fc2c919d0d6d2f6b2c608f5849d27227a72f99d33ccf01b3fd59e3282e2c9626b8eba4e45d4266a742f61bb2ff506ddbfba0892d0c9 languageName: node linkType: hard -"@types/chai-spies@npm:~1.0.5": - version: 1.0.5 - resolution: "@types/chai-spies@npm:1.0.5" +"@types/chai-spies@npm:~1.0.6": + version: 1.0.6 + resolution: "@types/chai-spies@npm:1.0.6" dependencies: "@types/chai": "*" - checksum: d8009e0e4bf78fdce4a6824d43a2e206ddc0c07e3869f5d55ed7a7476c5623f77d0a5f444148ce7a0b9d9f47592fe3f0c6652952e6a0f514d8603cbb2e587cd4 + checksum: 2f4e1fd3ed4f317b6f445a4516612b2a40e25c86cf60ba093ce3569012d612d0fc05c96d69223221c26fad60d0f3af75f7c2bb1858e48b209e8da8f0a1d5fccd languageName: node linkType: hard -"@types/chai@npm:*, @types/chai@npm:~4.3.16": +"@types/chai@npm:*": version: 4.3.16 resolution: "@types/chai@npm:4.3.16" checksum: bb5f52d1b70534ed8b4bf74bd248add003ffe1156303802ea367331607c06b494da885ffbc2b674a66b4f90c9ee88759790a5f243879f6759f124f22328f5e95 languageName: node linkType: hard +"@types/chai@npm:~4.3.19": + version: 4.3.19 + resolution: "@types/chai@npm:4.3.19" + checksum: abd4d3239735054f3b6e8163e45bc6495f66469729fbcf4784c9f2b82361a6845d45ab9c518818c78eafa46d015e3a72306e9949d1333e10d7eaedf426af4261 + languageName: node + linkType: hard + "@types/chalk@npm:^2.2.0": version: 2.2.0 resolution: "@types/chalk@npm:2.2.0" @@ -12944,21 +13051,21 @@ __metadata: languageName: node linkType: hard -"@types/chart.js@npm:^2.9.39": - version: 2.9.39 - resolution: "@types/chart.js@npm:2.9.39" +"@types/chart.js@npm:^2.9.41": + version: 2.9.41 + resolution: "@types/chart.js@npm:2.9.41" dependencies: moment: ^2.10.2 - checksum: c19cab03143db9cabc9c7277b77a974634baa9a9554dc95a7d2996091ac88ca9d98718e701446932a4cf0b8f3c2862ad0d3f232e1deb6a9bdf72d9ef5342582d + checksum: 7c6b29c837d35edaa371e05850e10468d6690dec1128f247b108c3cd15903c80cafb158a3cbd6a90beb0a9e1e33b12759251e1e63d6cbfa5995a0c5dc0df58b1 languageName: node linkType: hard -"@types/codemirror@npm:^5.60.12": - version: 5.60.12 - resolution: "@types/codemirror@npm:5.60.12" +"@types/codemirror@npm:^5.60.15": + version: 5.60.15 + resolution: "@types/codemirror@npm:5.60.15" dependencies: "@types/tern": "*" - checksum: dff22f32ea42ccd3f9bfcf408631f94a11ffb4614ff4fa8cc55adf7da6e7ba96650533b8dd27d037242747bdaa85141e93520f84409db7bc394862a174a10e1e + checksum: cfad3f569de48fba3efa44fdfeba77933e231486a52cc80cff7ce6eeeed5b447a5bc2b11e2226bc00ccee332c661e53e35a15cf14eb835f434a6a402d9462f5f languageName: node linkType: hard @@ -12981,19 +13088,19 @@ __metadata: languageName: node linkType: hard -"@types/cookie-parser@npm:^1.4.5": - version: 1.4.5 - resolution: "@types/cookie-parser@npm:1.4.5" +"@types/cookie-parser@npm:^1.4.7": + version: 1.4.7 + resolution: "@types/cookie-parser@npm:1.4.7" dependencies: "@types/express": "*" - checksum: 45855721706d6a57bb0441db11fb59db407414ea83a0000a0df80d19230447cc3bcc43c436397bfb8998ec78db222265fa8083456b00c5b5054a0c03d06f0086 + checksum: 7b87c59420598e686a57e240be6e0db53967c3c8814be9326bf86609ee2fc39c4b3b9f2263e1deba43526090121d1df88684b64c19f7b494a80a4437caf3d40b languageName: node linkType: hard -"@types/cookie@npm:^0.5.3": - version: 0.5.3 - resolution: "@types/cookie@npm:0.5.3" - checksum: b785618f6b2fdceb6a20a17e1dfe99651b1e5c2c079f486de76dfb21b508f09d91913755e4a6dbdfe628882ea32466bb2e9318b114ce34efa5e624356494fcab +"@types/cookie@npm:^0.5.4": + version: 0.5.4 + resolution: "@types/cookie@npm:0.5.4" + checksum: bd9603ce5e9bcbe1c2c9fabe3d1b1da131f1db10a7fa0077b17eb06d59bf4a6192fbb890597e8e2295067a078aaf8299bc662bfe993e8c2d1ecd62f859317ece languageName: node linkType: hard @@ -13004,12 +13111,12 @@ __metadata: languageName: node linkType: hard -"@types/cors@npm:^2.8.15": - version: 2.8.15 - resolution: "@types/cors@npm:2.8.15" +"@types/cors@npm:^2.8.17": + version: 2.8.17 + resolution: "@types/cors@npm:2.8.17" dependencies: "@types/node": "*" - checksum: ef7b0aba4c6a4c1fe9d459bd471ebaa891a75319682c9248daa17720003d1d0d2c59de4bdb6868630596ade9b7c3c949e652d6141b14c6fe4387ffcc520d0f3f + checksum: 469bd85e29a35977099a3745c78e489916011169a664e97c4c3d6538143b0a16e4cc72b05b407dc008df3892ed7bf595f9b7c0f1f4680e169565ee9d64966bde languageName: node linkType: hard @@ -13020,10 +13127,10 @@ __metadata: languageName: node linkType: hard -"@types/cssom@npm:^0.4.2": - version: 0.4.2 - resolution: "@types/cssom@npm:0.4.2" - checksum: 4bcc54245b8c09c832c21465f60af412a5c2446ae5c17ab1a874dd87a9488e43951f8027594cc0eaf99d4e9a7c061882f017552a13f0a94ac25db9ac92fea837 +"@types/cssom@npm:^0.4.3": + version: 0.4.3 + resolution: "@types/cssom@npm:0.4.3" + checksum: 3a04921a41fd353e3484962ed705385847b5aeb740988d7c773691417673d01b7cf79d0093fd2b57515d86a0e3ad76dc804b63def8be48ae6f1fdcebd2ab4cd9 languageName: node linkType: hard @@ -13380,12 +13487,12 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.1.10": - version: 4.1.10 - resolution: "@types/debug@npm:4.1.10" +"@types/debug@npm:^4.1.12": + version: 4.1.12 + resolution: "@types/debug@npm:4.1.12" dependencies: "@types/ms": "*" - checksum: 938f79c5b610f851da9c67ecd8641a09b33ce9cb38fe4c9f4d20ee743d6bccb5d8e9a833a4cd23e0684a316622af67a0634fa706baea5a01f5219961d1976314 + checksum: 47876a852de8240bfdaf7481357af2b88cb660d30c72e73789abf00c499d6bc7cd5e52f41c915d1b9cd8ec9fef5b05688d7b7aef17f7f272c2d04679508d1053 languageName: node linkType: hard @@ -13407,17 +13514,17 @@ __metadata: languageName: node linkType: hard -"@types/ejson@npm:^2.2.1": - version: 2.2.1 - resolution: "@types/ejson@npm:2.2.1" - checksum: 8a0e6e9d50a9b33cdb645e7fdaca5fccb5a0687be0918eaeb06c9c7f21bb8b5757881de77c9d19d84bf16e6949c10b23d098954990aef7bd26257d471f4c008f +"@types/ejson@npm:^2.2.2": + version: 2.2.2 + resolution: "@types/ejson@npm:2.2.2" + checksum: 3f2eb25d98f93821156b071e647bca626563900ecb34bb81cdba33721ee7d046bb66db7d6de664312b9480821821a1d177cf5e146dc1f10adcf43cbd2e387bbe languageName: node linkType: hard -"@types/emojione@npm:^2.2.8": - version: 2.2.8 - resolution: "@types/emojione@npm:2.2.8" - checksum: 3342fd3fbcbc7e7429c7a23330f559d0f41f4fcba5a699035fddb53efba8c7c5ed3af54024011643aa338792d8fd963768766877bc115e093bef193a391abb05 +"@types/emojione@npm:^2.2.9": + version: 2.2.9 + resolution: "@types/emojione@npm:2.2.9" + checksum: ec16f96f8d9886dc3f82bd8aa3ee34f320069771d9a82d01c3c20da9c0c13c7b07347eff2912b914caf3416e3cedea4b963ecc3331c23d743270c975a91ff4d4 languageName: node linkType: hard @@ -13431,7 +13538,7 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:~8.44.6": +"@types/eslint@npm:*": version: 8.44.6 resolution: "@types/eslint@npm:8.44.6" dependencies: @@ -13441,6 +13548,16 @@ __metadata: languageName: node linkType: hard +"@types/eslint@npm:~8.44.9": + version: 8.44.9 + resolution: "@types/eslint@npm:8.44.9" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: 6f8889e94e67a5e43c15f5a2530798f864ace08c270bfb3f153cb705da4e30a80e0e9a0fc05317c8642c8dda909d528968172089eb4d52aca9f212761df25d90 + languageName: node + linkType: hard + "@types/estree@npm:*, @types/estree@npm:^1.0.0": version: 1.0.1 resolution: "@types/estree@npm:1.0.1" @@ -13476,7 +13593,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.13, @types/express@npm:^4.17.20, @types/express@npm:^4.17.8, @types/express@npm:^4.7.0": +"@types/express@npm:*, @types/express@npm:^4.16.1, @types/express@npm:^4.17.13, @types/express@npm:^4.17.8, @types/express@npm:^4.7.0": version: 4.17.20 resolution: "@types/express@npm:4.17.20" dependencies: @@ -13500,19 +13617,19 @@ __metadata: languageName: node linkType: hard -"@types/fibers@npm:^3.1.3": - version: 3.1.3 - resolution: "@types/fibers@npm:3.1.3" - checksum: c4511eecea1c4e73a4b4310ff5152bb43477dfbd7c19ea8614de5ce6d604c6bd629b4552bdbc06fbd593215c684d06aee4e6d8e6681b4ab019f2b8a03838de79 +"@types/fibers@npm:^3.1.4": + version: 3.1.4 + resolution: "@types/fibers@npm:3.1.4" + checksum: 36bb70198fb5b7f99b010c006ad0e77d473061cf03d4f63f1842040b7ecb62e8800041c9509b927ec59a1d5ac374d5e7c86b2fe0de2a9813f1538ab0248e5dea languageName: node linkType: hard -"@types/gc-stats@npm:^1.4.2": - version: 1.4.2 - resolution: "@types/gc-stats@npm:1.4.2" +"@types/gc-stats@npm:^1.4.3": + version: 1.4.3 + resolution: "@types/gc-stats@npm:1.4.3" dependencies: "@types/node": "*" - checksum: 4039f699b497595c3ac30f221f72d798e12a764093ab5df05cec25cd501fe806005daec10c25737d2b08449772bc318ac720f01fe73d13fdb33168429e25483f + checksum: 983ad3841a1f62a1014e4c0becf81d258f07dc44849598079168df6f4ea293eb8abef7896b6847b9dd68e61b4628525279950b3bb4a75c045b8d461cbaaef3e9 languageName: node linkType: hard @@ -13533,10 +13650,10 @@ __metadata: languageName: node linkType: hard -"@types/google-libphonenumber@npm:^7.4.29": - version: 7.4.29 - resolution: "@types/google-libphonenumber@npm:7.4.29" - checksum: 63b1d03ab6dcd877c1249251d73c54777f6b6cbd13f6b3107c8fcef70d19f8e3a768555942735318da86fa6c50ea0dcfc82d521d4defc9e9dfc1e67b4746ab96 +"@types/google-libphonenumber@npm:^7.4.30": + version: 7.4.30 + resolution: "@types/google-libphonenumber@npm:7.4.30" + checksum: 09270ed030076a0e69965ad5bad564e672a110e1e83c9092f6d264c3e0355921631521cb9afd022018a51cddfd19ba52d0421f4682d7b68b836dbb52537320de languageName: node linkType: hard @@ -13549,10 +13666,10 @@ __metadata: languageName: node linkType: hard -"@types/gravatar@npm:^1.8.5": - version: 1.8.5 - resolution: "@types/gravatar@npm:1.8.5" - checksum: a4a5e9010e5f2caff27f360cd4e0acf4e3276be3fdf8cc4a38baed8b9ca14d187b6b5c1d30d4d3abe326b0aecf30d1b47111ea543c8f6caf8bce9a8bb147c090 +"@types/gravatar@npm:^1.8.6": + version: 1.8.6 + resolution: "@types/gravatar@npm:1.8.6" + checksum: b690f2816321358e8528fe0b731e5c50905ed2c4859327156a87b717a59b45363a2f2c1398c3a9015fa45b17d71d86b1e828fd3df542d13ba946745d9a26094e languageName: node linkType: hard @@ -13605,28 +13722,28 @@ __metadata: languageName: node linkType: hard -"@types/i18next-sprintf-postprocessor@npm:^0.2.2": - version: 0.2.2 - resolution: "@types/i18next-sprintf-postprocessor@npm:0.2.2" +"@types/i18next-sprintf-postprocessor@npm:^0.2.3": + version: 0.2.3 + resolution: "@types/i18next-sprintf-postprocessor@npm:0.2.3" dependencies: i18next: ">=17.0.11" - checksum: 1029c5f896e453534a128013f9ee8a0873a2e03b82a18a530842d06618c3dd1b1f3adf0491efa3f7efd86d19c61b23af60df5c0f36306ed298a20d6d895db95e + checksum: d647c6bb75e2e038ee82d2e5bac762c0254d4fe31232f2fba5f35945779e8509cfa687746219280e2a126e33a647fd459641e8353cf922ddcd2a4e528a1234ba languageName: node linkType: hard -"@types/identity-obj-proxy@npm:^3": +"@types/identity-obj-proxy@npm:^3.0.2": version: 3.0.2 resolution: "@types/identity-obj-proxy@npm:3.0.2" checksum: 77387ee587657ab24f12a1dee5c0e1386358d5c38cda5cac78bc5049340cb358e009f6254de3bbdee6a08e46f13b1552cd47a0bbd3e7a53ff469bf58a04ec6e9 languageName: node linkType: hard -"@types/imap@npm:^0.8.39": - version: 0.8.39 - resolution: "@types/imap@npm:0.8.39" +"@types/imap@npm:^0.8.40": + version: 0.8.40 + resolution: "@types/imap@npm:0.8.40" dependencies: "@types/node": "*" - checksum: 403a0810dc24f035d1762d9ebc1cf548448bdbe565ab6062d2a1d1be2afd4f8ef1483723d4077c0e0e2312a39a27f393a513a40515589149c0617aa984946ae5 + checksum: 5bd33aeeca13bd3403f685048c7c8e136502dc3f5703d732748b488193c96b79295b0901ff1dd72a5cc6b826fc33572bf7376cd6533194797903f9d58cfa8e6d languageName: node linkType: hard @@ -13690,7 +13807,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:*, @types/jest@npm:~29.5.12": +"@types/jest@npm:*": version: 29.5.12 resolution: "@types/jest@npm:29.5.12" dependencies: @@ -13700,6 +13817,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:~29.5.13": + version: 29.5.13 + resolution: "@types/jest@npm:29.5.13" + dependencies: + expect: ^29.0.0 + pretty-format: ^29.0.0 + checksum: 875ac23c2398cdcf22aa56c6ba24560f11d2afda226d4fa23936322dde6202f9fdbd2b91602af51c27ecba223d9fc3c1e33c9df7e47b3bf0e2aefc6baf13ce53 + languageName: node + linkType: hard + "@types/jquery@npm:*": version: 3.5.14 resolution: "@types/jquery@npm:3.5.14" @@ -13709,19 +13836,19 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4.0.8": - version: 4.0.8 - resolution: "@types/js-yaml@npm:4.0.8" - checksum: a5a77a5a1eac7e7fb667156c251c2b947ca4ddfdda570726369dd50bd5b2b1d0da2d0fb4273d1b10aa1782406d7b3da8923d957df4fb89dbfa1db06f43297de2 +"@types/js-yaml@npm:^4.0.9": + version: 4.0.9 + resolution: "@types/js-yaml@npm:4.0.9" + checksum: e5e5e49b5789a29fdb1f7d204f82de11cb9e8f6cb24ab064c616da5d6e1b3ccfbf95aa5d1498a9fbd3b9e745564e69b4a20b6c530b5a8bbb2d4eb830cda9bc69 languageName: node linkType: hard -"@types/jsdom-global@npm:^3.0.6": - version: 3.0.6 - resolution: "@types/jsdom-global@npm:3.0.6" +"@types/jsdom-global@npm:^3.0.7": + version: 3.0.7 + resolution: "@types/jsdom-global@npm:3.0.7" dependencies: "@types/jsdom": "*" - checksum: cb216d588d6abc583615706bbc8beab6265d7afec8e51cadea06f84dd5ba4508e583c500556b2c273d7569c80af9cd68dce19ab6042cd881567aa0fe10261cd1 + checksum: 7b6e58419b56c69fc1b1ef4df8180f19bf171eaa06d1dcc073e87f4cdeb172538b1b2fc16bc783e9521a7630787bad3d0607f4ac66730e5b27525eb73656b86f languageName: node linkType: hard @@ -13770,10 +13897,10 @@ __metadata: languageName: node linkType: hard -"@types/jsrsasign@npm:^10.5.11": - version: 10.5.11 - resolution: "@types/jsrsasign@npm:10.5.11" - checksum: c18b52af99ffb831fd84738356a49e94407e732f0a88f0f6db1e1fd55c06a47d2bab23a9e17b3b21af50fbf5f4d115e13fbe2796357204b18072929bd7b4bd5c +"@types/jsrsasign@npm:^10.5.14": + version: 10.5.14 + resolution: "@types/jsrsasign@npm:10.5.14" + checksum: 04e48fa2a225b6108eb6399a2485ed597fabe4dbd6cf02309abfca9da3e6028697d0fb20e4399b1ef340d7f1a70ace70746aa156ca0c2785663cb2e15797e653 languageName: node linkType: hard @@ -13784,10 +13911,10 @@ __metadata: languageName: node linkType: hard -"@types/katex@npm:~0.16.5": - version: 0.16.5 - resolution: "@types/katex@npm:0.16.5" - checksum: a1ce22cd87acd9b32891931f2bc4355c3540cc0a423e161a2e5b040d3e50812cb85ce1fd09f23d42324b19f9da30ded6b1807114f215624f670d79bb46c47cc8 +"@types/katex@npm:~0.16.7": + version: 0.16.7 + resolution: "@types/katex@npm:0.16.7" + checksum: 4fd15d93553be97c02c064e16be18d7ccbabf66ec72a9dc7fd5bfa47f0c7581da2f942f693c7cb59499de4c843c2189796e49c9647d336cbd52b777b6722a95a languageName: node linkType: hard @@ -13800,10 +13927,10 @@ __metadata: languageName: node linkType: hard -"@types/later@npm:^1.2.8": - version: 1.2.8 - resolution: "@types/later@npm:1.2.8" - checksum: b89b391e6dc6721955b04c01b81a3e38b4ac3d4f40ebcf35bc34d694c46a81c387c028760b3fc1441b9852c46298dfd562708f61504aba65983d2484b99d6420 +"@types/later@npm:^1.2.9": + version: 1.2.9 + resolution: "@types/later@npm:1.2.9" + checksum: dc0db51bfbeb0caa4528e10725728349824dbe1ddbce6acd5258370bde586bb6ee67880d543e7e8dbb178c4001ba6db2e71cddfbcbdd7a1c523d49fbb26b61fb languageName: node linkType: hard @@ -13816,10 +13943,10 @@ __metadata: languageName: node linkType: hard -"@types/less@npm:~3.0.5": - version: 3.0.5 - resolution: "@types/less@npm:3.0.5" - checksum: 72d04c7877a63ef8a49e22a2aaf2e595aec41b4fcbbb48603e4d3a3d1e2d903edea6654db00bba52148ea4101a904e866b410599088b41935aa4c0733d25ef3c +"@types/less@npm:~3.0.6": + version: 3.0.6 + resolution: "@types/less@npm:3.0.6" + checksum: f1e5a7b7da6c0e65e0881a563d71287243f8cdeee1abb3e72ed9d449b4fb5421deee9b3bbd3bff856f17870563c2fc8cc4674c1f77632a73a9d713200dd7505a languageName: node linkType: hard @@ -13839,21 +13966,21 @@ __metadata: languageName: node linkType: hard -"@types/lodash.debounce@npm:^4.0.8": - version: 4.0.8 - resolution: "@types/lodash.debounce@npm:4.0.8" +"@types/lodash.debounce@npm:^4.0.9": + version: 4.0.9 + resolution: "@types/lodash.debounce@npm:4.0.9" dependencies: "@types/lodash": "*" - checksum: 63f195cb053ca390135a9aca62bb60fa149ca81838519871506b60760ff4113333709becb8e4147707eaa3d916dab7eff66b0588caf4ce508cabda9bee9c5b60 + checksum: 8183a152e01928e3b97ca773f6ae6038b8695e76493ba8bf6b743ec143948a62294fbc9d49fa4a78b52265b3ba4892ef57534e0c13d04aa0f111671b5a944feb languageName: node linkType: hard -"@types/lodash.get@npm:^4.4.8": - version: 4.4.8 - resolution: "@types/lodash.get@npm:4.4.8" +"@types/lodash.get@npm:^4.4.9": + version: 4.4.9 + resolution: "@types/lodash.get@npm:4.4.9" dependencies: "@types/lodash": "*" - checksum: fea09c12f098e5cbdc16510e8319a7f0dd4d0af49a5e6622ae4f0dcd4893f1b2c44a5d22f452d73443f50b85c8461f3fe2370c19b8d9051686265bbb0aae0ffa + checksum: d7e071d267e3d1bab58db14b6ce63fc51ca3c9c9697d3548cd5c0f16c08a7ce2188cff75dff1858b5623c3bd070836857211ebef4f364c53b82e31f3190fe0b4 languageName: node linkType: hard @@ -13864,13 +13991,13 @@ __metadata: languageName: node linkType: hard -"@types/mailparser@npm:^3.4.3": - version: 3.4.3 - resolution: "@types/mailparser@npm:3.4.3" +"@types/mailparser@npm:^3.4.4": + version: 3.4.4 + resolution: "@types/mailparser@npm:3.4.4" dependencies: "@types/node": "*" iconv-lite: ^0.6.3 - checksum: 9374b713311b523b66429a7b509e90229dd001029e87d901ab6c756e1856eb2a8427012c016ad70ac45b39056eba1334e568f85ce9f329f5dd147986a897b691 + checksum: c8bdb579d66756042b75c3763dd8b7b2fab6657d7e7d33504bb65bbbe6fa220b8da2ae67918dd821716c3229d8803bedffb00f2314a8946da1856396187c646a languageName: node linkType: hard @@ -13907,12 +14034,12 @@ __metadata: languageName: node linkType: hard -"@types/meteor-collection-hooks@npm:^0.8.8": - version: 0.8.8 - resolution: "@types/meteor-collection-hooks@npm:0.8.8" +"@types/meteor-collection-hooks@npm:^0.8.9": + version: 0.8.9 + resolution: "@types/meteor-collection-hooks@npm:0.8.9" dependencies: meteor-typings: ^1.3.1 - checksum: bf0afc8531c836f3f7ead4aa0f8be3f69257af7a62397924a50533a20837b21b16c307777bb4fe52dc2e26753ba7269d0df6133ba6f1dad27d4075fea38a05f9 + checksum: 7b12838587a6d411e3c44c682532cd43a4db99d4b11164867a2831a45593d2c637fb7feeede661b05eb6a63d8fa48228c0b5a0cb61e556c291e4d06f9f457892 languageName: node linkType: hard @@ -14012,19 +14139,19 @@ __metadata: languageName: node linkType: hard -"@types/node-gcm@npm:^1.0.3": - version: 1.0.3 - resolution: "@types/node-gcm@npm:1.0.3" - checksum: 232e3d401f381fe9312343efb920892a7aa2f63457e00608ff2c3f0d1bff4a3707affe9a9e484ffead39272cfba0d1791901b7e937420233455dd047d63a4587 +"@types/node-gcm@npm:^1.0.5": + version: 1.0.5 + resolution: "@types/node-gcm@npm:1.0.5" + checksum: 5448aa9de99b23700a2cc243483ed08bffd988a91d7431ed193dfbb55fc5c087c58310f728992a83f0e653b9edf9220434b62a0fabc5c7f417067ca805c1f92c languageName: node linkType: hard -"@types/node-rsa@npm:^1.1.3": - version: 1.1.3 - resolution: "@types/node-rsa@npm:1.1.3" +"@types/node-rsa@npm:^1.1.4": + version: 1.1.4 + resolution: "@types/node-rsa@npm:1.1.4" dependencies: "@types/node": "*" - checksum: 03606729a96722623e316b1728d45ba452e008e01e886127d1d964221119dd4196be724ec062fc81be5beb28ce8e5fcc9c55109275cdae60545ddc293b89e77d + checksum: cd607463b7954dd0eda390e0710fa5fdc1a26e716baefd77134f54893ed57bfbf1906f3a354f43f89174aa37ac6f05fa450b708e516a4a8fa643e0d2f8e26106 languageName: node linkType: hard @@ -14044,21 +14171,28 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^14.0.10 || ^16.0.0, @types/node@npm:^14.14.20 || ^16.0.0, @types/node@npm:^16.18.60": +"@types/node@npm:^14.0.10 || ^16.0.0, @types/node@npm:^14.14.20 || ^16.0.0": version: 16.18.60 resolution: "@types/node@npm:16.18.60" checksum: aa0c81c3f20e663584bf17a5968e54c419277af7982ef41f9d83edd1b7ab4c8af2583a3c8a9e1cf659c6307e6f787e1be20522855121371f5a46d1d54f8a70e3 languageName: node linkType: hard -"@types/node@npm:^14.0.26, @types/node@npm:^14.14.37, @types/node@npm:^14.18.63, @types/node@npm:~14.18.42": +"@types/node@npm:^14.0.26, @types/node@npm:^14.14.37, @types/node@npm:^14.18.63, @types/node@npm:~14.18.63": version: 14.18.63 resolution: "@types/node@npm:14.18.63" checksum: be909061a54931778c71c49dc562586c32f909c4b6197e3d71e6dac726d8bd9fccb9f599c0df99f52742b68153712b5097c0f00cac4e279fa894b0ea6719a8fd languageName: node linkType: hard -"@types/nodemailer@npm:*, @types/nodemailer@npm:^6.4.13": +"@types/node@npm:^16.18.108": + version: 16.18.108 + resolution: "@types/node@npm:16.18.108" + checksum: 5029ce7b0d247690360f2698da4fe765ef5cd5e2409b63b3a45e90bbf7bbaaf3497e769336fb7725be04a221d6e934e3a4616ec7fc205812943def3ea9d70862 + languageName: node + linkType: hard + +"@types/nodemailer@npm:*": version: 6.4.13 resolution: "@types/nodemailer@npm:6.4.13" dependencies: @@ -14067,6 +14201,15 @@ __metadata: languageName: node linkType: hard +"@types/nodemailer@npm:^6.4.15": + version: 6.4.15 + resolution: "@types/nodemailer@npm:6.4.15" + dependencies: + "@types/node": "*" + checksum: f6f9a2f8a669703ecc3ca6359c12345b16f6b2e5691b93c406b9af7de639c02092ec00133526e6fecd8c60d884890a7cd0f967d8e64bedab46d5c3d8be0882d7 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -14081,19 +14224,19 @@ __metadata: languageName: node linkType: hard -"@types/oauth2-server@npm:^3.0.15": - version: 3.0.15 - resolution: "@types/oauth2-server@npm:3.0.15" +"@types/oauth2-server@npm:^3.0.17": + version: 3.0.17 + resolution: "@types/oauth2-server@npm:3.0.17" dependencies: "@types/express": "*" - checksum: b6c73ec0ba3a83e7a9cc1bf0e75fd640a76bcf57742642775abf950d851478a686e3f54ad8e24a09772e1dd89ed953742418c76ebb7df12b97b68f1cb94dae80 + checksum: f45224356b9a3736b86f3ed1a8873f7f535e10b22af06bc7d34eefd96af4f5db83ed4b618b0103566828fe279e8bed2ee304f6644a31bd127470a5a49398ca49 languageName: node linkType: hard -"@types/object-path@npm:^0.11.3": - version: 0.11.3 - resolution: "@types/object-path@npm:0.11.3" - checksum: 9c2f1ec11d9d15df1682ab1483c0f51cac3ff8c3a951662a50d15fd48824fc43a052041503d0761790a29a4100e33989eb519889948c3592d0bf6bde59d3ec79 +"@types/object-path@npm:^0.11.4": + version: 0.11.4 + resolution: "@types/object-path@npm:0.11.4" + checksum: 7f1f5cb18b651d21e7861da176d8f87526c936ed949a8126a2692195cbe65734ed1a1a22c06a24a54afe1890483a3d6b074b402ebfca7a7567c1c287b588f563 languageName: node linkType: hard @@ -14125,24 +14268,24 @@ __metadata: languageName: node linkType: hard -"@types/parseurl@npm:^1.3.2": - version: 1.3.2 - resolution: "@types/parseurl@npm:1.3.2" +"@types/parseurl@npm:^1.3.3": + version: 1.3.3 + resolution: "@types/parseurl@npm:1.3.3" dependencies: "@types/node": "*" - checksum: bfe0ad3222a957a1d8d21a5378a61a3ce21f0ce45ddfdd98fc727207b4bd3caf590d2a448c39d08774949e485a5f5f08429f6025a6b0123456a8a7fb9270ddb2 + checksum: 2ea8d3b9615850fce3dd208f980aaca2c82c6cb20e033ee5705adbc773ea167fc57ee72045ba521fef0f6ad292fddaf91e398dad9edec10b9829621a8556446f languageName: node linkType: hard -"@types/polka@npm:^0.5.6": - version: 0.5.6 - resolution: "@types/polka@npm:0.5.6" +"@types/polka@npm:^0.5.7": + version: 0.5.7 + resolution: "@types/polka@npm:0.5.7" dependencies: "@types/express": "*" "@types/express-serve-static-core": "*" "@types/node": "*" "@types/trouter": "*" - checksum: e956629526782b722fe134a93e159774d13d58a66796f5b6f763b04cd5c426439a8556962e1da8e8a61574ed1ad6239ffbef28cd8c03b4f8828a9a4b03189dde + checksum: ec9bd7ae06b6cd49c972cd3af750c29dd6de28bf2aaf7e710ef2eec839636ee6efe8cf354008b80f9a865675cb3cedc60aab1bf9737bf4f5835d1c527b4f64c8 languageName: node linkType: hard @@ -14160,10 +14303,10 @@ __metadata: languageName: node linkType: hard -"@types/prometheus-gc-stats@npm:^0.6.3": - version: 0.6.3 - resolution: "@types/prometheus-gc-stats@npm:0.6.3" - checksum: b8ad00f21c2d7f19819b0d82b778f4919847e44f1f74a570fb52386fa7c5250af3a5e2eef5b8aa69cbf7fd8cc68f31f91bd7a002ce5b3722bb971da31af543a0 +"@types/prometheus-gc-stats@npm:^0.6.4": + version: 0.6.4 + resolution: "@types/prometheus-gc-stats@npm:0.6.4" + checksum: 31d9c6a26c55829aa57c05ef602897926b5a068655b9e9850de92a7b7106a9c5e7971d96dc95d082817303899781cd98a319cd05699bc1ef5e9932456c23fd2a languageName: node linkType: hard @@ -14181,26 +14324,26 @@ __metadata: languageName: node linkType: hard -"@types/proxy-from-env@npm:^1.0.3": - version: 1.0.3 - resolution: "@types/proxy-from-env@npm:1.0.3" +"@types/proxy-from-env@npm:^1.0.4": + version: 1.0.4 + resolution: "@types/proxy-from-env@npm:1.0.4" dependencies: "@types/node": "*" - checksum: 3661687ae4bd90b9c41e504d3e3124b1ccd7d39d5877a7930fc3630989e37bde4e6b9055ea719d4b85c400a872bee02336ee5e187380f075cec021a4aebced44 + checksum: cfa34200e6ddb19c78957c2c661212c8eee1f7dbc1931e918909300b47cc0336023c2a526972c494cb5409010fd35edba37bd795893796cc50e1efabd34e3969 languageName: node linkType: hard -"@types/proxyquire@npm:^1.3.30": - version: 1.3.30 - resolution: "@types/proxyquire@npm:1.3.30" - checksum: e247d0afdb59aae942313112ca6429b0cbe410b0a858796c63e16ea945b3a7686f0660a03c685d79f10ed807051f645a763e13aa72d3725aeea74fd40901fdad +"@types/proxyquire@npm:^1.3.31": + version: 1.3.31 + resolution: "@types/proxyquire@npm:1.3.31" + checksum: 945024495fc991f6152686795ac6f2f2d0f571834e67fa41c1e84877eeb1a321a24ab8ff67fc5152d9227f5b39f6254213dced15deaf80ed7443059c2257e072 languageName: node linkType: hard -"@types/psl@npm:^1.1.2": - version: 1.1.2 - resolution: "@types/psl@npm:1.1.2" - checksum: fc0a7ae56ca53157035226d964f5a37749187804c07787d25a3f8e0235130c277b52d027139d1a7058d7826014a8019d68d46e2719b0404ac8545d39d41fc43a +"@types/psl@npm:^1.1.3": + version: 1.1.3 + resolution: "@types/psl@npm:1.1.3" + checksum: adc24b5f0a2569563367437cbdcbb63c780e798360a4c21470043acda2adad56e510c9f357be36f297974d68534821febc38652709e5d712ac2cdd62f9a6777a languageName: node linkType: hard @@ -14225,21 +14368,21 @@ __metadata: languageName: node linkType: hard -"@types/react-beautiful-dnd@npm:^13.1.6": - version: 13.1.6 - resolution: "@types/react-beautiful-dnd@npm:13.1.6" +"@types/react-beautiful-dnd@npm:^13.1.8": + version: 13.1.8 + resolution: "@types/react-beautiful-dnd@npm:13.1.8" dependencies: "@types/react": "*" - checksum: 437c315cac4455fd0085150d5d163330e523e965193f642f45edf34eb56098e68a6f956d90ecb11abfcb03ce34177e5445c1489da46353ee5a6a6306414fb936 + checksum: f71c64ba7e2e1f8480e772b45268856d2bf99adf90d12dd5f31486075dfd3e33a0b0922969851c660f01aa9593aa38b38e17ee9038eb866af9ec4327be903cb9 languageName: node linkType: hard -"@types/react-dom@npm:~17.0.22": - version: 17.0.22 - resolution: "@types/react-dom@npm:17.0.22" +"@types/react-dom@npm:~17.0.25": + version: 17.0.25 + resolution: "@types/react-dom@npm:17.0.25" dependencies: "@types/react": ^17 - checksum: 3c24331c0a2211370968befaad107598ee4f796c08e9b0de0b0126ee598a7c51f7bf4e0e7d9a76522a3b60d91f091d46131d136bcbe49aeb6ec3b8480dff03f9 + checksum: d1e582682478e0848c8d54ea3e89d02047bac6d916266b85ce63731b06987575919653ea7159d98fda47ade3362b8c4d5796831549564b83088e7aa9ce8b60ed languageName: node linkType: hard @@ -14264,7 +14407,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^17, @types/react@npm:~17.0.69": +"@types/react@npm:*, @types/react@npm:^17": version: 17.0.69 resolution: "@types/react@npm:17.0.69" dependencies: @@ -14275,6 +14418,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:~17.0.80": + version: 17.0.80 + resolution: "@types/react@npm:17.0.80" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": ^0.16 + csstype: ^3.0.2 + checksum: 1c27bfc42305d77ef0da55f8f6d4c4a3471aa02b294fcf29ea0f2cfb0bf02892e5a0a3bc7559fa4a5ba50697b2e31076cb5aa5987f69cfc2e880f6426edb8bdf + languageName: node + linkType: hard + "@types/readdir-glob@npm:*": version: 1.1.1 resolution: "@types/readdir-glob@npm:1.1.1" @@ -14300,10 +14454,10 @@ __metadata: languageName: node linkType: hard -"@types/rewire@npm:^2.5.29": - version: 2.5.29 - resolution: "@types/rewire@npm:2.5.29" - checksum: 2ad2f1134fe7c350fc88f0a9cc17eb49377f1a61776cde10b62bae0f5956dedd97adbb8da0e3ca8ddfb7034699a89fe2f9dc31af35f11727c4b38c8a089877c3 +"@types/rewire@npm:^2.5.30": + version: 2.5.30 + resolution: "@types/rewire@npm:2.5.30" + checksum: 75f756ee068ad7f8aac697dc80f0007afa8c837e7aa211fd95ba7eeea8905043b20439e9b1c8326be77643159580aeebc49a464e626b0837ee188bd8a05dfe84 languageName: node linkType: hard @@ -14323,6 +14477,13 @@ __metadata: languageName: node linkType: hard +"@types/scheduler@npm:^0.16": + version: 0.16.8 + resolution: "@types/scheduler@npm:0.16.8" + checksum: 6c091b096daa490093bf30dd7947cd28e5b2cd612ec93448432b33f724b162587fed9309a0acc104d97b69b1d49a0f3fc755a62282054d62975d53d7fd13472d + languageName: node + linkType: hard + "@types/semver@npm:^7.3.10, @types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.4 resolution: "@types/semver@npm:7.5.4" @@ -14407,12 +14568,12 @@ __metadata: languageName: node linkType: hard -"@types/speakeasy@npm:^2.0.9": - version: 2.0.9 - resolution: "@types/speakeasy@npm:2.0.9" +"@types/speakeasy@npm:^2.0.10": + version: 2.0.10 + resolution: "@types/speakeasy@npm:2.0.10" dependencies: "@types/node": "*" - checksum: 4f3d2217f96625c481f9220a5d16a776c7003d9025fafe728f91f07ba815ac2233db4df85cf4a721b5e042a478f793eee6e4e54c397288f3a39e3f04aa8375ac + checksum: 672c757a7662895a0d9eea5ecc8309b75c3925f00f0af4e7133fd3e475f3fdb1289d95ae6cf7fbab024f149703d999891b17771cc221db4e9a24c70ab1b04319 languageName: node linkType: hard @@ -14432,10 +14593,10 @@ __metadata: languageName: node linkType: hard -"@types/strict-uri-encode@npm:^2.0.1": - version: 2.0.1 - resolution: "@types/strict-uri-encode@npm:2.0.1" - checksum: 63d62f43d20583428d112f6a9de5a444f31f8e314a8c57b9a59523f99ae3152e41e2e39ff0fb0404171135402d2226aba21a5db64de0d0d385ec8c11d5a0f4ce +"@types/strict-uri-encode@npm:^2.0.2": + version: 2.0.2 + resolution: "@types/strict-uri-encode@npm:2.0.2" + checksum: e395868cc1cd49537be56d2f43b417ddc11559f807d5e1402d180870d0e2872fbe4d35765de1bbec76b939faf77cfdb13aec715d9946f6675e17073169a70dcd languageName: node linkType: hard @@ -14458,16 +14619,16 @@ __metadata: languageName: node linkType: hard -"@types/supertest@npm:^2.0.15": - version: 2.0.15 - resolution: "@types/supertest@npm:2.0.15" +"@types/supertest@npm:^2.0.16": + version: 2.0.16 + resolution: "@types/supertest@npm:2.0.16" dependencies: "@types/superagent": "*" - checksum: 89c1983662f0ab20969b3a6c44344397fd222d0f78b282619aabbe817f7c88a64210fd2b8b8f075ea22a27084e30ebc287bc5105619cbbf9af7f008e77f6eb93 + checksum: 2fc998ea698e0467cdbe3bea0ebce2027ea3a45a13e51a6cecb0435f44b486faecf99c34d8702d2d7fe033e6e09fdd2b374af52ecc8d0c69a1deec66b8c0dd52 languageName: node linkType: hard -"@types/supports-color@npm:~7.2.0": +"@types/supports-color@npm:~7.2.1": version: 7.2.1 resolution: "@types/supports-color@npm:7.2.1" checksum: abf7d9348deadf5386cf5faec062a4132e647a179584f52cace87435248f520be73c58ac28618cf5684e6b0ed6bb635d5a975cc71ff613af7db2d5648557ef45 @@ -14490,10 +14651,10 @@ __metadata: languageName: node linkType: hard -"@types/textarea-caret@npm:^3.0.2": - version: 3.0.2 - resolution: "@types/textarea-caret@npm:3.0.2" - checksum: 6989477fa7be544cb84ffbb3302d5c4c2632e7900e0fcc12adf9bd78f4d0ebbf38db9392cb39c8fa791aba935013e1b14aeb1c222f328a67de6202f945331e73 +"@types/textarea-caret@npm:^3.0.3": + version: 3.0.3 + resolution: "@types/textarea-caret@npm:3.0.3" + checksum: 61d4b08391c28f8b37236e65946457819460de1749b860a852c01cfb1093ab2ceb3fe11b54f788e7c024cb580d26984b9285438139f3cb3bb333150955413a8e languageName: node linkType: hard @@ -14525,10 +14686,10 @@ __metadata: languageName: node linkType: hard -"@types/ua-parser-js@npm:^0.7.38": - version: 0.7.38 - resolution: "@types/ua-parser-js@npm:0.7.38" - checksum: 8a44887f7c782ed4c59c4d9cb254674f2ff41d8f653da0c2bd6d4ace79cc3de1ce5648b77f906e337fb97328e1114583e54c3b8882d194e1b8cb281f407a1ef7 +"@types/ua-parser-js@npm:^0.7.39": + version: 0.7.39 + resolution: "@types/ua-parser-js@npm:0.7.39" + checksum: 81046605eb2815b098228743b7dfde887cc8990369f2ad56e71f1400b4cef5078481c7ca91ca3dddf2e8d4e183fe93224bfdeee13bfe034a1e62d55cfbac9e26 languageName: node linkType: hard @@ -14555,17 +14716,17 @@ __metadata: languageName: node linkType: hard -"@types/use-subscription@npm:^1.0.1": - version: 1.0.1 - resolution: "@types/use-subscription@npm:1.0.1" - checksum: f9e3535d40dbcd606e7ef306120c52a78b9da7a94ebe0b56af8f8326984588b2db12fbc841e2b2d719728810816afa9c7d64b40a849048186a4308e2f3ff0339 +"@types/use-subscription@npm:^1.0.2": + version: 1.0.2 + resolution: "@types/use-subscription@npm:1.0.2" + checksum: e547d8ffdf8410c985c271a30903cd754687a6514a67192378213590f707a0f516e0c11930cba10bd7e98dc4f7917e066b3b8d37a622d02b6fcf3c5df530e467 languageName: node linkType: hard -"@types/use-sync-external-store@npm:^0.0.5": - version: 0.0.5 - resolution: "@types/use-sync-external-store@npm:0.0.5" - checksum: 96a22fa059d8a6d0fe0b03e5157eb22f599ab1cc58b9441617dec4be6d8586260fcf9041912ab90e92d9c6ea6dfec5c758bae6418552aa687fc7e0bb904e68bc +"@types/use-sync-external-store@npm:^0.0.6": + version: 0.0.6 + resolution: "@types/use-sync-external-store@npm:0.0.6" + checksum: a95ce330668501ad9b1c5b7f2b14872ad201e552a0e567787b8f1588b22c7040c7c3d80f142cbb9f92d13c4ea41c46af57a20f2af4edf27f224d352abcfe4049 languageName: node linkType: hard @@ -14576,7 +14737,7 @@ __metadata: languageName: node linkType: hard -"@types/uuid@npm:^9": +"@types/uuid@npm:^9.0.8": version: 9.0.8 resolution: "@types/uuid@npm:9.0.8" checksum: b8c60b7ba8250356b5088302583d1704a4e1a13558d143c549c408bf8920535602ffc12394ede77f8a8083511b023704bc66d1345792714002bfa261b17c5275 @@ -14642,14 +14803,14 @@ __metadata: languageName: node linkType: hard -"@types/webpack@npm:^5.28.4": - version: 5.28.4 - resolution: "@types/webpack@npm:5.28.4" +"@types/webpack@npm:^5.28.5": + version: 5.28.5 + resolution: "@types/webpack@npm:5.28.5" dependencies: "@types/node": "*" tapable: ^2.2.0 webpack: ^5 - checksum: 7a08e31096a05c77bb49b3830f85f2e340090fa2b19dbe4681c7eab92e49db85bdbbe7b374df7d7aa38549922ff3252f43a00d4c5c2f9fe0c2a198cdb5ddde7e + checksum: 14359d9ccecef7ef1ea271c00baec5337213c7fda63a34c61b9e519505b3928d0807cdbb5b1172d1994e1179920b89c57eaf2cbf64599958b67cd70720ac2a9b languageName: node linkType: hard @@ -14688,7 +14849,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.5.1, @types/ws@npm:^8.5.5, @types/ws@npm:^8.5.8": +"@types/ws@npm:^8.5.1, @types/ws@npm:^8.5.5": version: 8.5.8 resolution: "@types/ws@npm:8.5.8" dependencies: @@ -14697,22 +14858,31 @@ __metadata: languageName: node linkType: hard -"@types/xml-crypto@npm:~1.4.4": - version: 1.4.4 - resolution: "@types/xml-crypto@npm:1.4.4" +"@types/ws@npm:^8.5.12": + version: 8.5.12 + resolution: "@types/ws@npm:8.5.12" + dependencies: + "@types/node": "*" + checksum: ddefb6ad1671f70ce73b38a5f47f471d4d493864fca7c51f002a86e5993d031294201c5dced6d5018fb8905ad46888d65c7f20dd54fc165910b69f42fba9a6d0 + languageName: node + linkType: hard + +"@types/xml-crypto@npm:~1.4.6": + version: 1.4.6 + resolution: "@types/xml-crypto@npm:1.4.6" dependencies: "@types/node": "*" xpath: 0.0.27 - checksum: c19616b531b26d7f3fafee6165d7be42969bfa2ef7184f501c489c80515d483425fb97e55ae816d91cf2f92e765e81f0a7cfc97c5dedb46f829edb677b154a55 + checksum: e53516a2f5e4e018e164eb1cb9fc922294b9a339624e567c1c00a2b1496e9f86826210473e62ceb0b45949638c9d149da088b3598f6b3acd86e933f0a2b23f2c languageName: node linkType: hard -"@types/xml-encryption@npm:~1.2.3": - version: 1.2.3 - resolution: "@types/xml-encryption@npm:1.2.3" +"@types/xml-encryption@npm:~1.2.4": + version: 1.2.4 + resolution: "@types/xml-encryption@npm:1.2.4" dependencies: "@types/node": "*" - checksum: 1f678c5cb7378702dd1bfbbe1ec3b4dd62d7b8350c225866df109d60c8c7c4e39b20d32193ead175b1c68944ce22ffcae470685371e6257bf0bfe45f7f366aec + checksum: 1ef957dfb47cf55b12e114755e271a2343f73eb4c59ab6c68b0b7d1b8111d7e1bd8d2bfe0601d2aea09be83c66355bc77fc59f9b71aeff9bb9e15371bcfef5d3 languageName: node linkType: hard @@ -16409,6 +16579,17 @@ __metadata: languageName: node linkType: hard +"asn1.js@npm:^4.10.1": + version: 4.10.1 + resolution: "asn1.js@npm:4.10.1" + dependencies: + bn.js: ^4.0.0 + inherits: ^2.0.1 + minimalistic-assert: ^1.0.0 + checksum: 9289a1a55401238755e3142511d7b8f6fc32f08c86ff68bd7100da8b6c186179dd6b14234fba2f7f6099afcd6758a816708485efe44bc5b2a6ec87d9ceeddbb5 + languageName: node + linkType: hard + "asn1.js@npm:^5.2.0": version: 5.4.1 resolution: "asn1.js@npm:5.4.1" @@ -16447,15 +16628,16 @@ __metadata: languageName: node linkType: hard -"assert@npm:^2.0.0": - version: 2.0.0 - resolution: "assert@npm:2.0.0" +"assert@npm:^2.1.0": + version: 2.1.0 + resolution: "assert@npm:2.1.0" dependencies: - es6-object-assign: ^1.1.0 - is-nan: ^1.2.1 - object-is: ^1.0.1 - util: ^0.12.0 - checksum: bb91f181a86d10588ee16c5e09c280f9811373974c29974cbe401987ea34e966699d7989a812b0e19377b511ea0bc627f5905647ce569311824848ede382cae8 + call-bind: ^1.0.2 + is-nan: ^1.3.2 + object-is: ^1.1.5 + object.assign: ^4.1.4 + util: ^0.12.5 + checksum: 1ed1cabba9abe55f4109b3f7292b4e4f3cf2953aad8dc148c0b3c3bd676675c31b1abb32ef563b7d5a19d1715bf90d1e5f09fad2a4ee655199468902da80f7c2 languageName: node linkType: hard @@ -16784,6 +16966,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.6.6": + version: 1.6.6 + resolution: "b4a@npm:1.6.6" + checksum: c46a27e3ac9c84426ae728f0fc46a6ae7703a7bc03e771fa0bef4827fd7cf3bb976d1a3d5afff54606248372ab8fdf595bd0114406690edf37f14d120630cf7f + languageName: node + linkType: hard + "babel-jest@npm:^29.0.3, babel-jest@npm:^29.5.0, babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -16831,7 +17020,7 @@ __metadata: languageName: node linkType: hard -"babel-loader@npm:~9.1.2": +"babel-loader@npm:~9.1.3": version: 9.1.3 resolution: "babel-loader@npm:9.1.3" dependencies: @@ -17118,6 +17307,50 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.0.0, bare-events@npm:^2.2.0": + version: 2.4.2 + resolution: "bare-events@npm:2.4.2" + checksum: 6cd2b10dd32a3410787e120c091b6082fbc2df0c45ed723a7ae51d0e2f55d2a4037e1daff21dae90b671d36582f9f8d50df337875c281d10adb60df81b8cd861 + languageName: node + linkType: hard + +"bare-fs@npm:^2.1.1": + version: 2.3.5 + resolution: "bare-fs@npm:2.3.5" + dependencies: + bare-events: ^2.0.0 + bare-path: ^2.0.0 + bare-stream: ^2.0.0 + checksum: 071b1dff94a213eaf0b41693953959bf10af2deade597a56ff206a5d833579d56bc8530aa4614bb88bf39fd6d52f2404f7c36af4695109ffa756a13837ac3d91 + languageName: node + linkType: hard + +"bare-os@npm:^2.1.0": + version: 2.4.4 + resolution: "bare-os@npm:2.4.4" + checksum: e90088a7dc0307c020350a28df8ec5564cae5a4b7a213d8509d70831d7064308e2ed31de801b68f474cb004ad3a0a66bd28c38374d270484d9025ee71af20396 + languageName: node + linkType: hard + +"bare-path@npm:^2.0.0, bare-path@npm:^2.1.0": + version: 2.1.3 + resolution: "bare-path@npm:2.1.3" + dependencies: + bare-os: ^2.1.0 + checksum: 20301aeb05b735852a396515464908e51e896922c3bb353ef2a09ff54e81ced94e6ad857bb0a36d2ce659c42bd43dd5c3d5643edd8faaf910ee9950c4e137b88 + languageName: node + linkType: hard + +"bare-stream@npm:^2.0.0": + version: 2.3.0 + resolution: "bare-stream@npm:2.3.0" + dependencies: + b4a: ^1.6.6 + streamx: ^2.20.0 + checksum: 17de9dbd5a6d70863b6e55f0acdfe1cb5d2b05f22d87e79986372cc796095eb4882a868ee6ba3dc543243085d27f618b4b81ef2bf384bc1c690dd3a557b6e30d + languageName: node + linkType: hard + "base-64@npm:^1.0.0": version: 1.0.0 resolution: "base-64@npm:1.0.0" @@ -17429,6 +17662,13 @@ __metadata: languageName: node linkType: hard +"bn.js@npm:^5.2.1": + version: 5.2.1 + resolution: "bn.js@npm:5.2.1" + checksum: 3dd8c8d38055fedfa95c1d5fc3c99f8dd547b36287b37768db0abab3c239711f88ff58d18d155dd8ad902b0b0cee973747b7ae20ea12a09473272b0201c9edd3 + languageName: node + linkType: hard + "bodec@npm:^0.1.0": version: 0.1.0 resolution: "bodec@npm:0.1.0" @@ -17456,7 +17696,27 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.2, body-parser@npm:^1.19.0, body-parser@npm:^1.20.2": +"body-parser@npm:1.20.3, body-parser@npm:^1.20.3": + version: 1.20.3 + resolution: "body-parser@npm:1.20.3" + dependencies: + bytes: 3.1.2 + content-type: ~1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: ~1.6.18 + unpipe: 1.0.0 + checksum: 1a35c59a6be8d852b00946330141c4f142c6af0f970faa87f10ad74f1ee7118078056706a05ae3093c54dabca9cd3770fa62a170a85801da1a4324f04381167d + languageName: node + linkType: hard + +"body-parser@npm:^1.19.0": version: 1.20.2 resolution: "body-parser@npm:1.20.2" dependencies: @@ -17657,7 +17917,7 @@ __metadata: languageName: node linkType: hard -"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4": +"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" dependencies: @@ -17671,7 +17931,7 @@ __metadata: languageName: node linkType: hard -"browserify-cipher@npm:^1.0.0": +"browserify-cipher@npm:^1.0.0, browserify-cipher@npm:^1.0.1": version: 1.0.1 resolution: "browserify-cipher@npm:1.0.1" dependencies: @@ -17694,7 +17954,7 @@ __metadata: languageName: node linkType: hard -"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.0.1": +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.0.1, browserify-rsa@npm:^4.1.0": version: 4.1.0 resolution: "browserify-rsa@npm:4.1.0" dependencies: @@ -17721,6 +17981,24 @@ __metadata: languageName: node linkType: hard +"browserify-sign@npm:^4.2.3": + version: 4.2.3 + resolution: "browserify-sign@npm:4.2.3" + dependencies: + bn.js: ^5.2.1 + browserify-rsa: ^4.1.0 + create-hash: ^1.2.0 + create-hmac: ^1.1.7 + elliptic: ^6.5.5 + hash-base: ~3.0 + inherits: ^2.0.4 + parse-asn1: ^5.1.7 + readable-stream: ^2.3.8 + safe-buffer: ^5.2.1 + checksum: 403a8061d229ae31266670345b4a7c00051266761d2c9bbeb68b1a9bcb05f68143b16110cf23a171a5d6716396a1f41296282b3e73eeec0a1871c77f0ff4ee6b + languageName: node + linkType: hard + "browserify-zlib@npm:^0.2.0": version: 0.2.0 resolution: "browserify-zlib@npm:0.2.0" @@ -18282,23 +18560,23 @@ __metadata: languageName: node linkType: hard -"chai-as-promised@npm:^7.1.1": - version: 7.1.1 - resolution: "chai-as-promised@npm:7.1.1" +"chai-as-promised@npm:^7.1.2": + version: 7.1.2 + resolution: "chai-as-promised@npm:7.1.2" dependencies: check-error: ^1.0.2 peerDependencies: - chai: ">= 2.1.2 < 5" - checksum: 7262868a5b51a12af4e432838ddf97a893109266a505808e1868ba63a12de7ee1166e9d43b5c501a190c377c1b11ecb9ff8e093c89f097ad96c397e8ec0f8d6a + chai: ">= 2.1.2 < 6" + checksum: 671ee980054eb23a523875c1d22929a2ac05d89b5428e1fd12800f54fc69baf41014667b87e2368e2355ee2a3140d3e3d7d5a1f8638b07cfefd7fe38a149e3f6 languageName: node linkType: hard -"chai-datetime@npm:^1.8.0": - version: 1.8.0 - resolution: "chai-datetime@npm:1.8.0" +"chai-datetime@npm:^1.8.1": + version: 1.8.1 + resolution: "chai-datetime@npm:1.8.1" dependencies: chai: ">1.9.0" - checksum: 37752addc6de4134117342609be85292dcd21cc0ec40b9ed2415c06d75c3a3b4a6d39fe6a675229a973f374abcaf5d2b9ea4c0d88d75508d1ddc641b509aafb0 + checksum: 29d3320a18c5e809e198cc7ed20ebf053eef4081f64a43e91eb388dafe8ad6f3d560fde478d9f599f35be75a95fb33ed40ff22aa9f05f3c047b1b7df2176cb24 languageName: node linkType: hard @@ -18920,10 +19198,10 @@ __metadata: languageName: node linkType: hard -"codemirror@npm:^5.65.15": - version: 5.65.15 - resolution: "codemirror@npm:5.65.15" - checksum: 30e0cff9bfb2265b94fa6766e13975cb71db228e114d6d8cdcc160b495e32b0ff921ac09959715e3fef30a48c5a9d0655ffd0ff6c5fe7024656add438bb2b058 +"codemirror@npm:^5.65.17": + version: 5.65.17 + resolution: "codemirror@npm:5.65.17" + checksum: 8bc853524c6416826364d776b012f488b3f4736899e5c8026062f43927e09de773051dd1b34e8cfd25642d7e358679ca5b113f0034fdd6a295f4193b04f8c528 languageName: node linkType: hard @@ -19590,7 +19868,7 @@ __metadata: languageName: node linkType: hard -"create-ecdh@npm:^4.0.0": +"create-ecdh@npm:^4.0.0, create-ecdh@npm:^4.0.4": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" dependencies: @@ -19764,7 +20042,7 @@ __metadata: languageName: node linkType: hard -"crypto-browserify@npm:^3.11.0, crypto-browserify@npm:^3.12.0": +"crypto-browserify@npm:^3.11.0": version: 3.12.0 resolution: "crypto-browserify@npm:3.12.0" dependencies: @@ -19947,13 +20225,13 @@ __metadata: languageName: node linkType: hard -"css-vars-ponyfill@npm:^2.4.8": - version: 2.4.8 - resolution: "css-vars-ponyfill@npm:2.4.8" +"css-vars-ponyfill@npm:^2.4.9": + version: 2.4.9 + resolution: "css-vars-ponyfill@npm:2.4.9" dependencies: balanced-match: ^1.0.2 get-css-data: ^2.0.2 - checksum: ea2e270455d039d4b9a34e7a9c0264052c0c9d832538123afce7766cfe833b7c149406d5d984a406ad401d9b7e0115fdbac029321214d49ebe88cd743d055899 + checksum: 0535b54e42ddd9aa7fcda42db98e828c3cab58ffa6d69325d3fe794d3ec3f9bd6f03bdc870a183890a7e6f1fe2b2ef85b1e13431724a0703bb047c98ee083ffc languageName: node linkType: hard @@ -21124,7 +21402,7 @@ __metadata: languageName: node linkType: hard -"diffie-hellman@npm:^5.0.0": +"diffie-hellman@npm:^5.0.0, diffie-hellman@npm:^5.0.3": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" dependencies: @@ -21176,12 +21454,12 @@ __metadata: languageName: node linkType: hard -"docker-compose@npm:^0.24.3": - version: 0.24.3 - resolution: "docker-compose@npm:0.24.3" +"docker-compose@npm:^0.24.8": + version: 0.24.8 + resolution: "docker-compose@npm:0.24.8" dependencies: yaml: ^2.2.2 - checksum: b2149eafb6e0a37ff4595044fe63d2fac23483afab06bca71cece78df4bae6f796b0a123854957addda77cc0559f205bc03cf3984ced816161e30e7f247d88e7 + checksum: 48f3564c46490f1f51899a144deb546b61450a76bffddb378379ac7702aa34b055e0237e0dc77507df94d7ad6f1f7daeeac27730230bce9aafe2e35efeda6b45 languageName: node linkType: hard @@ -21283,10 +21561,10 @@ __metadata: languageName: node linkType: hard -"domain-browser@npm:^4.22.0": - version: 4.22.0 - resolution: "domain-browser@npm:4.22.0" - checksum: e7ce1c19073e17dec35cfde050a3ddaac437d3ba8b870adabf9d5682e665eab3084df05de432dedf25b34303f0a2c71ac30f1cdba61b1aea018047b10de3d988 +"domain-browser@npm:^4.23.0": + version: 4.23.0 + resolution: "domain-browser@npm:4.23.0" + checksum: 95b772f5fa88300240694380e06e03868573acdf86ca392a58c78602d6536dca2097ad2469a1500bd23a1329b09992de846e0b66c364cbf5711a7fee3ee5dac9 languageName: node linkType: hard @@ -21600,7 +21878,7 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": +"elliptic@npm:^6.5.3": version: 6.5.4 resolution: "elliptic@npm:6.5.4" dependencies: @@ -21615,6 +21893,21 @@ __metadata: languageName: node linkType: hard +"elliptic@npm:^6.5.5, elliptic@npm:^6.5.7": + version: 6.5.7 + resolution: "elliptic@npm:6.5.7" + dependencies: + bn.js: ^4.11.9 + brorand: ^1.1.0 + hash.js: ^1.0.0 + hmac-drbg: ^1.0.1 + inherits: ^2.0.4 + minimalistic-assert: ^1.0.1 + minimalistic-crypto-utils: ^1.0.1 + checksum: af0ffddffdbc2fea4eeec74388cd73e62ed5a0eac6711568fb28071566319785df529c968b0bf1250ba4bc628e074b2d64c54a633e034aa6f0c6b152ceb49ab8 + languageName: node + linkType: hard + "email-validator@npm:^2.0.4": version: 2.0.4 resolution: "email-validator@npm:2.0.4" @@ -22083,13 +22376,6 @@ __metadata: languageName: node linkType: hard -"es6-object-assign@npm:^1.1.0": - version: 1.1.0 - resolution: "es6-object-assign@npm:1.1.0" - checksum: 8d4fdf63484d78b5c64cacc2c2e1165bc7b6a64b739d2a9db6a4dc8641d99cc9efb433cdd4dc3d3d6b00bfa6ce959694e4665e3255190339945c5f33b692b5d8 - languageName: node - linkType: hard - "es6-shim@npm:^0.35.5": version: 0.35.6 resolution: "es6-shim@npm:0.35.6" @@ -22481,21 +22767,21 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react-hooks@npm:^4.6.0, eslint-plugin-react-hooks@npm:~4.6.0": - version: 4.6.0 - resolution: "eslint-plugin-react-hooks@npm:4.6.0" +"eslint-plugin-react-hooks@npm:^4.6.2, eslint-plugin-react-hooks@npm:~4.6.2": + version: 4.6.2 + resolution: "eslint-plugin-react-hooks@npm:4.6.2" peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 - checksum: 23001801f14c1d16bf0a837ca7970d9dd94e7b560384b41db378b49b6e32dc43d6e2790de1bd737a652a86f81a08d6a91f402525061b47719328f586a57e86c3 + checksum: 395c433610f59577cfcf3f2e42bcb130436c8a0b3777ac64f441d88c5275f4fcfc89094cedab270f2822daf29af1079151a7a6579a8e9ea8cee66540ba0384c4 languageName: node linkType: hard -"eslint-plugin-react-refresh@npm:^0.4.4": - version: 0.4.4 - resolution: "eslint-plugin-react-refresh@npm:0.4.4" +"eslint-plugin-react-refresh@npm:^0.4.11": + version: 0.4.11 + resolution: "eslint-plugin-react-refresh@npm:0.4.11" peerDependencies: eslint: ">=7" - checksum: 6b93f43cef5f69c18751db3267ce6cc7cb88f07061df28fc12401be56d93f37134f2a794c760f51cd5f84c5e81d81b003ef761ca76e7674646808b82884aa356 + checksum: 55c9efb14c3dadf6422bb96af8a561d3f1385f1dc68e7ca9d388cc4adc69907644e1bb509ecfbb033a8ec19ea326beb99577c82d4a2530000809d897b08bf3d4 languageName: node linkType: hard @@ -23260,7 +23546,7 @@ __metadata: languageName: node linkType: hard -"fast-fifo@npm:^1.1.0, fast-fifo@npm:^1.2.0": +"fast-fifo@npm:^1.1.0, fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" checksum: 6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 @@ -23392,10 +23678,10 @@ __metadata: languageName: node linkType: hard -"fastest-validator@npm:^1.17.0": - version: 1.17.0 - resolution: "fastest-validator@npm:1.17.0" - checksum: 0a6240f6dc7b544b3aadf9367410ecd64590dbfe505e6a24c339df954dd216f3a714d64a224abeef531d0a40dc348efe6cdbf59372d28954ac450cc0b212e0ec +"fastest-validator@npm:^1.19.0": + version: 1.19.0 + resolution: "fastest-validator@npm:1.19.0" + checksum: 93a52bcfb9cd3e6117de3ce4c975d182d9870496f2bddc2a71a53d5894ad536c21c62497f120c8d5e396b2312070b8c03acbefdfca32c081d524618ac2ee01a8 languageName: node linkType: hard @@ -23915,7 +24201,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.10.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.14.9, follow-redirects@npm:^1.15.2": +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.10.0, follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.4, follow-redirects@npm:^1.14.7, follow-redirects@npm:^1.14.8, follow-redirects@npm:^1.14.9": version: 1.15.4 resolution: "follow-redirects@npm:1.15.4" peerDependenciesMeta: @@ -23925,6 +24211,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.6": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 859e2bacc7a54506f2bf9aacb10d165df78c8c1b0ceb8023f966621b233717dab56e8d08baadc3ad3b9db58af290413d585c999694b7c146aaf2616340c3d2a6 + languageName: node + linkType: hard + "fontkit@npm:^2.0.2": version: 2.0.2 resolution: "fontkit@npm:2.0.2" @@ -24387,6 +24683,17 @@ __metadata: languageName: node linkType: hard +"gc-stats@npm:^1.4.1": + version: 1.4.1 + resolution: "gc-stats@npm:1.4.1" + dependencies: + nan: ^2.18.0 + node-gyp: latest + node-gyp-build: ^4.8.0 + checksum: 81d9bfc884f1cdfcb4683d940eb52a0379cafba11ec2b993946463b6ca30350dcc77e8c62694b7d1ac81f71477f25a72d865181aa1e87368eb79500ebfbdfce0 + languageName: node + linkType: hard + "gcp-metadata@npm:^5.0.0": version: 5.0.0 resolution: "gcp-metadata@npm:5.0.0" @@ -24917,10 +25224,10 @@ __metadata: languageName: node linkType: hard -"google-libphonenumber@npm:^3.2.33": - version: 3.2.33 - resolution: "google-libphonenumber@npm:3.2.33" - checksum: d029c19c7278ac9acb446028c2e304df16341732905a0cf8a105ab595cb19c01456fd57d76e48a3220038044d8ce361edc539eea2bfd924ea36fb28178cf7dbe +"google-libphonenumber@npm:^3.2.38": + version: 3.2.38 + resolution: "google-libphonenumber@npm:3.2.38" + checksum: 5e30ced0399b3803e61d41c0f7a69e289baf0912568381a0f2f0a5f0e5d3403dd8a26dd758ecb52e56337d83f45865fb36c0d7bfec255868286f05cc03a6b920 languageName: node linkType: hard @@ -25288,6 +25595,16 @@ __metadata: languageName: node linkType: hard +"hash-base@npm:~3.0, hash-base@npm:~3.0.4": + version: 3.0.4 + resolution: "hash-base@npm:3.0.4" + dependencies: + inherits: ^2.0.1 + safe-buffer: ^5.0.1 + checksum: 878465a0dfcc33cce195c2804135352c590d6d10980adc91a9005fd377e77f2011256c2b7cfce472e3f2e92d561d1bf3228d2da06348a9017ce9a258b3b49764 + languageName: node + linkType: hard + "hash.js@npm:^1.0.0, hash.js@npm:^1.0.3, hash.js@npm:^1.1.7": version: 1.1.7 resolution: "hash.js@npm:1.1.7" @@ -26613,13 +26930,20 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.0.1, ipaddr.js@npm:^2.1.0": +"ipaddr.js@npm:^2.0.1": version: 2.1.0 resolution: "ipaddr.js@npm:2.1.0" checksum: 807a054f2bd720c4d97ee479d6c9e865c233bea21f139fb8dabd5a35c4226d2621c42e07b4ad94ff3f82add926a607d8d9d37c625ad0319f0e08f9f2bd1968e2 languageName: node linkType: hard +"ipaddr.js@npm:^2.2.0": + version: 2.2.0 + resolution: "ipaddr.js@npm:2.2.0" + checksum: 770ba8451fd9bf78015e8edac0d5abd7a708cbf75f9429ca9147a9d2f3a2d60767cd5de2aab2b1e13ca6e4445bdeff42bf12ef6f151c07a5c6cf8a44328e2859 + languageName: node + linkType: hard + "is-absolute-url@npm:^2.0.0": version: 2.1.0 resolution: "is-absolute-url@npm:2.1.0" @@ -27117,7 +27441,7 @@ __metadata: languageName: node linkType: hard -"is-nan@npm:^1.2.1": +"is-nan@npm:^1.3.2": version: 1.3.2 resolution: "is-nan@npm:1.3.2" dependencies: @@ -28849,14 +29173,14 @@ __metadata: languageName: node linkType: hard -"katex@npm:~0.16.9": - version: 0.16.9 - resolution: "katex@npm:0.16.9" +"katex@npm:~0.16.11": + version: 0.16.11 + resolution: "katex@npm:0.16.11" dependencies: commander: ^8.3.0 bin: katex: cli.js - checksum: 861194dfd4d86505e657f688fb73048d46ac498edafce71199502a35b03c0ecc35ba930c631be79c4a09d90a0d23476673cd52f6bc367c7a161854d64005fa95 + checksum: 49d9340705f4922ee22aacedad45664971449e5ca65e42a70228961336c8d4746c37c3c719bcc2114b6ad21182800c7d3d8bea28fe6f951fc45fe7e8322ea3bd languageName: node linkType: hard @@ -30309,35 +30633,35 @@ __metadata: languageName: node linkType: hard -"meteor-node-stubs@npm:^1.2.5": - version: 1.2.5 - resolution: "meteor-node-stubs@npm:1.2.5" +"meteor-node-stubs@npm:^1.2.10": + version: 1.2.10 + resolution: "meteor-node-stubs@npm:1.2.10" dependencies: - assert: ^2.0.0 + "@meteorjs/crypto-browserify": ^3.12.1 + assert: ^2.1.0 browserify-zlib: ^0.2.0 buffer: ^5.7.1 console-browserify: ^1.2.0 constants-browserify: ^1.0.0 - crypto-browserify: ^3.12.0 - domain-browser: ^4.22.0 - elliptic: ^6.5.4 + domain-browser: ^4.23.0 + elliptic: ^6.5.7 events: ^3.3.0 https-browserify: ^1.0.0 os-browserify: ^0.3.0 - path-browserify: ^1.0.0 + path-browserify: ^1.0.1 process: ^0.11.10 punycode: ^1.4.1 querystring-es3: ^0.2.1 - readable-stream: ^3.6.0 + readable-stream: ^3.6.2 stream-browserify: ^3.0.0 stream-http: ^3.2.0 string_decoder: ^1.3.0 timers-browserify: ^2.0.12 tty-browserify: 0.0.1 - url: ^0.11.0 - util: ^0.12.4 + url: ^0.11.4 + util: ^0.12.5 vm-browserify: ^1.1.2 - checksum: 2529bce377342b2c01f97c397fe89490fce0149ecb37dba1b18d2f865753a25addea2c16dd212afcaa6b9aa01abec52c90721b65653d15ff59708d5bd9adef15 + checksum: ce0e49991b2afba48dea0c5699bebb40502d8e775a486d4dca0f004432063ab56ceae2c4d22d8448d669edcb9cfcd7cb82e7a351b301b59f563ef4067daedf7c languageName: node linkType: hard @@ -30869,15 +31193,104 @@ __metadata: languageName: node linkType: hard -"moleculer@npm:^0.14.31": - version: 0.14.31 - resolution: "moleculer@npm:0.14.31" +"moleculer@npm:0.14.34": + version: 0.14.34 + resolution: "moleculer@npm:0.14.34" + dependencies: + args: ^5.0.3 + eventemitter2: ^6.4.9 + fastest-validator: ^1.19.0 + glob: ^7.2.0 + ipaddr.js: ^2.2.0 + kleur: ^4.1.5 + lodash: ^4.17.21 + lru-cache: ^6.0.0 + node-fetch: ^2.6.7 + recursive-watch: ^1.1.4 + peerDependencies: + amqplib: ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0 + avsc: ^5.0.0 + bunyan: ^1.0.0 + cbor-x: ^0.8.3 || ^0.9.0 || ^1.2.0 + dd-trace: ^0.33.0 || ^0.34.0 || ^0.35.0 || ^0.36.0 || >=1.0.0 <1.6.0 + debug: ^4.0.0 + etcd3: ^1.0.0 + ioredis: ^4.0.0 || ^5.0.0 + jaeger-client: ^3.0.0 + kafka-node: ^5.0.0 + log4js: ^6.0.0 + mqtt: ^4.0.0 || ^5.0.0 + msgpack5: ^5.0.0 || ^6.0.0 + nats: ^1.0.0 || ^2.0.0 + node-nats-streaming: ^0.0.51 || ^0.2.0 || ^0.3.0 + notepack.io: ^2.0.0 || ^3.0.0 + pino: ^6.0.0 || ^7.0.0 || ^8.0.0 + protobufjs: ^6.0.0 || ^7.0.0 + redlock: ^4.0.0 + rhea-promise: ^1.0.0 || ^2.0.0 + thrift: ^0.12.0 || ^0.16.0 + winston: ^3.0.0 + peerDependenciesMeta: + amqplib: + optional: true + avsc: + optional: true + bunyan: + optional: true + cbor-x: + optional: true + dd-trace: + optional: true + debug: + optional: true + etcd3: + optional: true + ioredis: + optional: true + jaeger-client: + optional: true + kafka-node: + optional: true + log4js: + optional: true + mqtt: + optional: true + msgpack5: + optional: true + nats: + optional: true + node-nats-streaming: + optional: true + notepack.io: + optional: true + pino: + optional: true + protobufjs: + optional: true + redlock: + optional: true + rhea-promise: + optional: true + thrift: + optional: true + winston: + optional: true + bin: + moleculer-runner: bin/moleculer-runner.js + moleculer-runner-esm: bin/moleculer-runner.mjs + checksum: 2f20a28d4796be8b5837ee963e2323ab13dc30064f0c9126dcc04b2969586a1b43c92d875349dc9cc0b36170d8dd640ddf843d022c6b6c803bc84216ec4fa7d9 + languageName: node + linkType: hard + +"moleculer@patch:moleculer@npm%3A0.14.34#./.yarn/patches/moleculer-npm-0.14.34-440e26767d.patch::locator=rocket.chat%40workspace%3A.": + version: 0.14.34 + resolution: "moleculer@patch:moleculer@npm%3A0.14.34#./.yarn/patches/moleculer-npm-0.14.34-440e26767d.patch::version=0.14.34&hash=e1c8f3&locator=rocket.chat%40workspace%3A." dependencies: args: ^5.0.3 eventemitter2: ^6.4.9 - fastest-validator: ^1.17.0 + fastest-validator: ^1.19.0 glob: ^7.2.0 - ipaddr.js: ^2.1.0 + ipaddr.js: ^2.2.0 kleur: ^4.1.5 lodash: ^4.17.21 lru-cache: ^6.0.0 @@ -30895,7 +31308,7 @@ __metadata: jaeger-client: ^3.0.0 kafka-node: ^5.0.0 log4js: ^6.0.0 - mqtt: ^4.0.0 + mqtt: ^4.0.0 || ^5.0.0 msgpack5: ^5.0.0 || ^6.0.0 nats: ^1.0.0 || ^2.0.0 node-nats-streaming: ^0.0.51 || ^0.2.0 || ^0.3.0 @@ -30954,11 +31367,11 @@ __metadata: bin: moleculer-runner: bin/moleculer-runner.js moleculer-runner-esm: bin/moleculer-runner.mjs - checksum: 4d6d05f98e2174b708de8c946901a4a3a1de411505f43909216a9d2ff93820e6eec7edd44b45c99ab04c6c312304a63a346288e181fead48558763fb4ec69c24 + checksum: a5aea2d8fd0cb1b79a64bd67e3e0ebe1d023f66bf268391124968866ca48d305853b9635e92b5e64420ed4bc4625e50a70d505255eede839d4bab1734aa7db91 languageName: node linkType: hard -"moment-timezone@npm:*, moment-timezone@npm:^0.5.43, moment-timezone@npm:^0.5.x, moment-timezone@npm:~0.5.43": +"moment-timezone@npm:*, moment-timezone@npm:^0.5.x": version: 0.5.43 resolution: "moment-timezone@npm:0.5.43" dependencies: @@ -30967,6 +31380,15 @@ __metadata: languageName: node linkType: hard +"moment-timezone@npm:^0.5.45, moment-timezone@npm:~0.5.45": + version: 0.5.45 + resolution: "moment-timezone@npm:0.5.45" + dependencies: + moment: ^2.29.4 + checksum: a22e9f983fbe1a01757ce30685bce92e3f6efa692eb682afd47b82da3ff960b3c8c2c3883ec6715c124bc985a342b57cba1f6ba25a1c8b4c7ad766db3cd5e1d0 + languageName: node + linkType: hard + "moment@npm:^2.10.2, moment@npm:^2.29.1, moment@npm:^2.29.4": version: 2.29.4 resolution: "moment@npm:2.29.4" @@ -31136,6 +31558,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.18.0": + version: 2.20.0 + resolution: "nan@npm:2.20.0" + dependencies: + node-gyp: latest + checksum: eb09286e6c238a3582db4d88c875db73e9b5ab35f60306090acd2f3acae21696c9b653368b4a0e32abcef64ee304a923d6223acaddd16169e5eaaf5c508fb533 + languageName: node + linkType: hard + "nanoid@npm:3.3.1": version: 3.3.1 resolution: "nanoid@npm:3.3.1" @@ -31154,6 +31585,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + "nanomatch@npm:^1.2.9": version: 1.2.13 resolution: "nanomatch@npm:1.2.13" @@ -31412,7 +31852,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.11, node-fetch@npm:^2.6.7, node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -31462,6 +31902,17 @@ __metadata: languageName: node linkType: hard +"node-gyp-build@npm:^4.8.0": + version: 4.8.2 + resolution: "node-gyp-build@npm:4.8.2" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 1a57bba8c4c193f808bd8ad1484d4ebdd8106dd9f04a3e82554dc716e3a2d87d7e369e9503c145e0e6a7e2c663fec0d8aaf52bd8156342ec7fc388195f37824e + languageName: node + linkType: hard + "node-gyp@npm:^9.4.1": version: 9.4.1 resolution: "node-gyp@npm:9.4.1" @@ -31988,7 +32439,7 @@ __metadata: languageName: node linkType: hard -"object-is@npm:^1.0.1, object-is@npm:^1.1.5": +"object-is@npm:^1.1.5": version: 1.1.6 resolution: "object-is@npm:1.1.6" dependencies: @@ -32734,6 +33185,20 @@ __metadata: languageName: node linkType: hard +"parse-asn1@npm:^5.1.7": + version: 5.1.7 + resolution: "parse-asn1@npm:5.1.7" + dependencies: + asn1.js: ^4.10.1 + browserify-aes: ^1.2.0 + evp_bytestokey: ^1.0.3 + hash-base: ~3.0 + pbkdf2: ^3.1.2 + safe-buffer: ^5.2.1 + checksum: 93c7194c1ed63a13e0b212d854b5213ad1aca0ace41c66b311e97cca0519cf9240f79435a0306a3b412c257f0ea3f1953fd0d9549419a0952c9e995ab361fd6c + languageName: node + linkType: hard + "parse-entities@npm:^2.0.0": version: 2.0.0 resolution: "parse-entities@npm:2.0.0" @@ -32859,7 +33324,7 @@ __metadata: languageName: node linkType: hard -"path-browserify@npm:^1.0.0, path-browserify@npm:^1.0.1": +"path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" checksum: c6d7fa376423fe35b95b2d67990060c3ee304fc815ff0a2dc1c6c3cfaff2bd0d572ee67e18f19d0ea3bbe32e8add2a05021132ac40509416459fffee35200699 @@ -33056,7 +33521,7 @@ __metadata: languageName: node linkType: hard -"pbkdf2@npm:^3.0.3": +"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" dependencies: @@ -33131,6 +33596,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.0.1": + version: 1.1.0 + resolution: "picocolors@npm:1.1.0" + checksum: a64d653d3a188119ff45781dfcdaeedd7625583f45280aea33fcb032c7a0d3959f2368f9b192ad5e8aade75b74dbd954ffe3106c158509a45e4c18ab379a2acd + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.0, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -33325,14 +33797,14 @@ __metadata: languageName: node linkType: hard -"playwright-qase-reporter@npm:^1.2.1": - version: 1.2.1 - resolution: "playwright-qase-reporter@npm:1.2.1" +"playwright-qase-reporter@npm:^1.2.2": + version: 1.2.2 + resolution: "playwright-qase-reporter@npm:1.2.2" dependencies: chalk: ^4.1.0 form-data: ^3.0.0 qaseio: ^2.0.2 - checksum: 8b4d2f5902a8d655b781e9dd6898d5d2f25739033f1fda2b600b07ac3fc7680f9a1ee917a969b7315f4f05211e36f1a6699155f9ead05a435afa695932bde9fa + checksum: c2d7c5e990357b5cc029d51179ca2af7b61fb7d871eeea05e78300749e7829ca5dd9eef00820ec45422b5036a06a3fa383c76b1dda052aadd490fdd66d6ffe15 languageName: node linkType: hard @@ -34223,7 +34695,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.2.15, postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.23, postcss@npm:~8.4.31": +"postcss@npm:^8.2.15, postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.23": version: 8.4.31 resolution: "postcss@npm:8.4.31" dependencies: @@ -34234,6 +34706,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:~8.4.45": + version: 8.4.45 + resolution: "postcss@npm:8.4.45" + dependencies: + nanoid: ^3.3.7 + picocolors: ^1.0.1 + source-map-js: ^1.2.0 + checksum: 3223cdad4a9392c0b334ee3ee7e4e8041c631cb6160609cef83c18d2b2580e931dd8068ab13cc6000c1a254d57492ac6c38717efc397c5dcc9756d06bc9c44f3 + languageName: node + linkType: hard + "postis@npm:^2.2.0": version: 2.2.0 resolution: "postis@npm:2.2.0" @@ -34367,7 +34850,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.7.1, prettier@npm:^2.8.4, prettier@npm:~2.8.7, prettier@npm:~2.8.8": +"prettier@npm:^2.7.1, prettier@npm:^2.8.4, prettier@npm:~2.8.8": version: 2.8.8 resolution: "prettier@npm:2.8.8" bin: @@ -34692,7 +35175,7 @@ __metadata: languageName: node linkType: hard -"public-encrypt@npm:^4.0.0": +"public-encrypt@npm:^4.0.0, public-encrypt@npm:^4.0.3": version: 4.0.3 resolution: "public-encrypt@npm:4.0.3" dependencies: @@ -34802,6 +35285,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:6.13.0, qs@npm:^6.12.3": + version: 6.13.0 + resolution: "qs@npm:6.13.0" + dependencies: + side-channel: ^1.0.6 + checksum: e9404dc0fc2849245107108ce9ec2766cde3be1b271de0bf1021d049dc5b98d1a2901e67b431ac5509f865420a7ed80b7acb3980099fe1c118a1c5d2e1432ad8 + languageName: node + linkType: hard + "qs@npm:6.9.3": version: 6.9.3 resolution: "qs@npm:6.9.3" @@ -34986,7 +35478,7 @@ __metadata: languageName: node linkType: hard -"randomfill@npm:^1.0.3": +"randomfill@npm:^1.0.3, randomfill@npm:^1.0.4": version: 1.0.4 resolution: "randomfill@npm:1.0.4" dependencies: @@ -35146,13 +35638,13 @@ __metadata: languageName: node linkType: hard -"re-resizable@npm:^6.9.9": - version: 6.9.9 - resolution: "re-resizable@npm:6.9.9" +"re-resizable@npm:^6.9.18": + version: 6.9.18 + resolution: "re-resizable@npm:6.9.18" peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - checksum: a2c8bfe86646fb02d5c9c624b1da26f9e6a5e2f552cd96ce4db690588bee6b21177065ce8e98646c6ca0b1a9c4ce233824b75eb346800d8248ac8a87b40f1b28 + checksum: 46ef26f45d4bf37f4db0d0758fdc0a1b97b29398ac0e8404c556118e15b5df7f47dba2acd60f9e4a9cc66ed536b9566c71d7fc0d2f9bf939609a3016da30704d languageName: node linkType: hard @@ -35219,21 +35711,20 @@ __metadata: languageName: node linkType: hard -"react-docgen-typescript-plugin@npm:^1.0.5, react-docgen-typescript-plugin@npm:~1.0.5": - version: 1.0.5 - resolution: "react-docgen-typescript-plugin@npm:1.0.5" +"react-docgen-typescript-plugin@npm:^1.0.8, react-docgen-typescript-plugin@npm:~1.0.8": + version: 1.0.8 + resolution: "react-docgen-typescript-plugin@npm:1.0.8" dependencies: debug: ^4.1.1 - endent: ^2.0.1 find-cache-dir: ^3.3.1 flat-cache: ^3.0.4 micromatch: ^4.0.2 react-docgen-typescript: ^2.2.2 - tslib: ^2.0.0 + tslib: ^2.6.2 peerDependencies: typescript: ">= 4.x" webpack: ">= 4" - checksum: 0f83d33c7b6dc82fef34ee820c94485c374d853774c5c26b04754ba3674fe4db2c7fc210b30fa0ca77c5033633553c12742aab6305a7f16cd263c70fedf27589 + checksum: c4fe1ae821be97270de370257747ec4f927f8ee291599f0e67b8bb13ba343da79894ac84a2836af428f877e88d1d96edbef6ee808ed23ecdb4b230fe4f268f1e languageName: node linkType: hard @@ -35833,6 +36324,21 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^2.3.8": + version: 2.3.8 + resolution: "readable-stream@npm:2.3.8" + dependencies: + core-util-is: ~1.0.0 + inherits: ~2.0.3 + isarray: ~1.0.0 + process-nextick-args: ~2.0.0 + safe-buffer: ~5.1.1 + string_decoder: ~1.1.1 + util-deprecate: ~1.0.1 + checksum: 65645467038704f0c8aaf026a72fbb588a9e2ef7a75cd57a01702ee9db1c4a1e4b03aaad36861a6a0926546a74d174149c8c207527963e0c2d3eee2f37678a42 + languageName: node + linkType: hard + "readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" @@ -35844,6 +36350,17 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^3.6.2": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + "readable-stream@npm:^4.0.0": version: 4.1.0 resolution: "readable-stream@npm:4.1.0" @@ -36670,11 +37187,11 @@ __metadata: resolution: "rocket.chat@workspace:." dependencies: "@changesets/cli": ^2.26.2 - "@types/chart.js": ^2.9.39 - "@types/js-yaml": ^4.0.8 + "@types/chart.js": ^2.9.41 + "@types/js-yaml": ^4.0.9 node-gyp: ^9.4.1 ts-node: ^10.9.2 - turbo: latest + turbo: ^2.1.2 languageName: unknown linkType: soft @@ -36693,16 +37210,16 @@ __metadata: "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/ui-kit": "workspace:~" - "@types/cookie": ^0.5.3 - "@types/cookie-parser": ^1.4.5 - "@types/ejson": ^2.2.1 - "@types/express": ^4.17.20 - "@types/fibers": ^3.1.3 + "@types/cookie": ^0.5.4 + "@types/cookie-parser": ^1.4.7 + "@types/ejson": ^2.2.2 + "@types/express": ^4.17.21 + "@types/fibers": ^3.1.4 "@types/node": ^14.18.63 - "@types/ws": ^8.5.8 + "@types/ws": ^8.5.12 ajv: ^8.11.0 bcrypt: ^5.0.1 - body-parser: ^1.20.2 + body-parser: ^1.20.3 colorette: ^2.0.20 cookie: ^0.5.0 cookie-parser: ^1.4.6 @@ -36712,7 +37229,7 @@ __metadata: fibers: ^5.0.3 jaeger-client: ^3.19.0 mem: ^8.1.1 - moleculer: ^0.14.31 + moleculer: ^0.14.34 mongodb: ^4.17.2 nats: ^2.6.1 npm-run-all: ^4.1.5 @@ -36721,7 +37238,7 @@ __metadata: pm2: ^5.2.0 sodium-native: ^3.3.0 sodium-plus: ^0.9.0 - ts-node: ^10.9.1 + ts-node: ^10.9.2 typescript: ~5.5.4 uuid: ^8.3.2 ws: ^8.8.1 @@ -37562,6 +38079,18 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" + dependencies: + call-bind: ^1.0.7 + es-errors: ^1.3.0 + get-intrinsic: ^1.2.4 + object-inspect: ^1.13.1 + checksum: bfc1afc1827d712271453e91b7cd3878ac0efd767495fd4e594c4c2afaa7963b7b510e249572bfd54b0527e66e4a12b61b80c061389e129755f34c493aad9b97 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -37877,6 +38406,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.0": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 4eb0cd997cdf228bc253bcaff9340afeb706176e64868ecd20efbe6efea931465f43955612346d6b7318789e5265bdc419bc7669c1cebe3db0eb255f57efa76b + languageName: node + linkType: hard + "source-map-resolve@npm:^0.5.0": version: 0.5.3 resolution: "source-map-resolve@npm:0.5.3" @@ -38282,15 +38818,15 @@ __metadata: languageName: node linkType: hard -"storybook-dark-mode@npm:~3.0.1": - version: 3.0.1 - resolution: "storybook-dark-mode@npm:3.0.1" +"storybook-dark-mode@npm:~3.0.3": + version: 3.0.3 + resolution: "storybook-dark-mode@npm:3.0.3" dependencies: "@storybook/addons": ^7.0.0 - "@storybook/api": ^7.0.0 "@storybook/components": ^7.0.0 "@storybook/core-events": ^7.0.0 "@storybook/global": ^5.0.0 + "@storybook/manager-api": ^7.0.0 "@storybook/theming": ^7.0.0 fast-deep-equal: ^3.1.3 memoizerific: ^1.11.3 @@ -38302,7 +38838,7 @@ __metadata: optional: true react-dom: optional: true - checksum: d04213c92e8a4af0035e80eb02b75b8da725ba7b1ecbfe050eb04cb4018d91394f08c8fe7c1b106c971b2047ef5a1ba776e78050ae1f6d7563cdfdba5e701a29 + checksum: 7db89470168e9d15f36c44554ddb80b5aeac7276664904adc7b2d1f1913a4a514b86769eb80d4fcc43070e75d4e79a53b2467e51d628298e2e35e3c47c30bc7f languageName: node linkType: hard @@ -38326,10 +38862,10 @@ __metadata: languageName: node linkType: hard -"stream-buffers@npm:^3.0.2": - version: 3.0.2 - resolution: "stream-buffers@npm:3.0.2" - checksum: b09fdeea606e3113ebd0e07010ed0cf038608fa396130add9e45deaff5cc3ba845dc25c31ad24f8341f85907846344cb7c85f75ea52c6572e2ac646e9b6072d0 +"stream-buffers@npm:^3.0.3": + version: 3.0.3 + resolution: "stream-buffers@npm:3.0.3" + checksum: 3f0bdc4b1fd3ff370cef5a2103dd930b8981d42d97741eeb087a660771e27f0fc35fa8a351bb36e15bbbbce0eea00fefed60d6cdff4c6c3f527580529f183807 languageName: node linkType: hard @@ -38419,6 +38955,21 @@ __metadata: languageName: node linkType: hard +"streamx@npm:^2.20.0": + version: 2.20.1 + resolution: "streamx@npm:2.20.1" + dependencies: + bare-events: ^2.2.0 + fast-fifo: ^1.3.2 + queue-tick: ^1.0.1 + text-decoder: ^1.1.0 + dependenciesMeta: + bare-events: + optional: true + checksum: 48605ddd3abdd86d2e3ee945ec7c9317f36abb5303347a8fff6e4c7926a72c33ec7ac86b50734ccd1cf65602b6a38e247966e8199b24e5a7485d9cec8f5327bd + languageName: node + linkType: hard + "strict-uri-encode@npm:^1.0.0": version: 1.1.0 resolution: "strict-uri-encode@npm:1.1.0" @@ -39174,7 +39725,7 @@ __metadata: languageName: node linkType: hard -"tar-fs@npm:^2.0.0, tar-fs@npm:^2.1.1": +"tar-fs@npm:^2.0.0": version: 2.1.1 resolution: "tar-fs@npm:2.1.1" dependencies: @@ -39197,6 +39748,23 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^3.0.5": + version: 3.0.6 + resolution: "tar-fs@npm:3.0.6" + dependencies: + bare-fs: ^2.1.1 + bare-path: ^2.1.0 + pump: ^3.0.0 + tar-stream: ^3.1.5 + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: b4fa09c70f75caf05bf5cf87369cd2862f1ac5fb75c4ddf9d25d55999f7736a94b58ad679d384196cba837c5f5ff14086e060fafccef5474a16e2d3058ffa488 + languageName: node + linkType: hard + "tar-stream@npm:^1.5.2, tar-stream@npm:^1.6.2": version: 1.6.2 resolution: "tar-stream@npm:1.6.2" @@ -39312,6 +39880,15 @@ __metadata: languageName: node linkType: hard +"telejson@npm:^7.2.0": + version: 7.2.0 + resolution: "telejson@npm:7.2.0" + dependencies: + memoizerific: ^1.11.3 + checksum: 55a3380c9ff3c5ad84581bb6bda28fc33c6b7c4a0c466894637da687639b8db0d21b0ff4c1bc1a7a92ae6b70662549d09e7b9e8b1ec334b2ef93078762ecdfb9 + languageName: node + linkType: hard + "temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0" @@ -39467,6 +40044,15 @@ __metadata: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.2.0 + resolution: "text-decoder@npm:1.2.0" + dependencies: + b4a: ^1.6.4 + checksum: 9f4c23900b42153af0e4a902577eba37cb70cd1d5b187732b81c74c705d3206952cf1dcecf97537794374f55aac6c547ac3860f1facc9560007ca9a06b0e309d + languageName: node + linkType: hard + "text-hex@npm:1.0.x": version: 1.0.0 resolution: "text-hex@npm:1.0.0" @@ -39876,7 +40462,7 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:~29.1.1": +"ts-jest@npm:~29.1.5": version: 29.1.5 resolution: "ts-jest@npm:29.1.5" dependencies: @@ -39912,7 +40498,7 @@ __metadata: languageName: node linkType: hard -"ts-loader@npm:~9.4.2": +"ts-loader@npm:~9.4.4": version: 9.4.4 resolution: "ts-loader@npm:9.4.4" dependencies: @@ -39927,7 +40513,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.9.1, ts-node@npm:^10.9.2, ts-node@npm:~10.9.1": +"ts-node@npm:^10.9.2, ts-node@npm:~10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: @@ -40032,6 +40618,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.6.2": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 + languageName: node + linkType: hard + "tsscmp@npm:^1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -40104,58 +40697,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.0.11": - version: 2.0.11 - resolution: "turbo-darwin-64@npm:2.0.11" +"turbo-darwin-64@npm:2.1.2": + version: 2.1.2 + resolution: "turbo-darwin-64@npm:2.1.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:2.0.11": - version: 2.0.11 - resolution: "turbo-darwin-arm64@npm:2.0.11" +"turbo-darwin-arm64@npm:2.1.2": + version: 2.1.2 + resolution: "turbo-darwin-arm64@npm:2.1.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:2.0.11": - version: 2.0.11 - resolution: "turbo-linux-64@npm:2.0.11" +"turbo-linux-64@npm:2.1.2": + version: 2.1.2 + resolution: "turbo-linux-64@npm:2.1.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:2.0.11": - version: 2.0.11 - resolution: "turbo-linux-arm64@npm:2.0.11" +"turbo-linux-arm64@npm:2.1.2": + version: 2.1.2 + resolution: "turbo-linux-arm64@npm:2.1.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:2.0.11": - version: 2.0.11 - resolution: "turbo-windows-64@npm:2.0.11" +"turbo-windows-64@npm:2.1.2": + version: 2.1.2 + resolution: "turbo-windows-64@npm:2.1.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:2.0.11": - version: 2.0.11 - resolution: "turbo-windows-arm64@npm:2.0.11" +"turbo-windows-arm64@npm:2.1.2": + version: 2.1.2 + resolution: "turbo-windows-arm64@npm:2.1.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:latest": - version: 2.0.11 - resolution: "turbo@npm:2.0.11" - dependencies: - turbo-darwin-64: 2.0.11 - turbo-darwin-arm64: 2.0.11 - turbo-linux-64: 2.0.11 - turbo-linux-arm64: 2.0.11 - turbo-windows-64: 2.0.11 - turbo-windows-arm64: 2.0.11 +"turbo@npm:^2.1.2": + version: 2.1.2 + resolution: "turbo@npm:2.1.2" + dependencies: + turbo-darwin-64: 2.1.2 + turbo-darwin-arm64: 2.1.2 + turbo-linux-64: 2.1.2 + turbo-linux-arm64: 2.1.2 + turbo-windows-64: 2.1.2 + turbo-windows-arm64: 2.1.2 dependenciesMeta: turbo-darwin-64: optional: true @@ -40171,7 +40764,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: a2fcb17b2549102dcd912799319a5c31cbabc3fcb76241bac1d2231ee4e1911789cd4e6b4eb050f9e8548ef89143ee77be59eb35b1843cf12b42f136ef176a0c + checksum: ee3d12a69fa512d30b325369c49a490cd2112547a59aca7bd3c9407b12bd9bd9ffd682e801c1e2e5711669c155ef06b0a090b0bbffa84fd60ddc5df0a1c855e6 languageName: node linkType: hard @@ -40410,7 +41003,7 @@ __metadata: languageName: node linkType: hard -"typedoc@npm:~0.24.1": +"typedoc@npm:~0.24.8": version: 0.24.8 resolution: "typedoc@npm:0.24.8" dependencies: @@ -40480,10 +41073,10 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^1.0.37": - version: 1.0.37 - resolution: "ua-parser-js@npm:1.0.37" - checksum: 4d481c720d523366d7762dc8a46a1b58967d979aacf786f9ceceb1cd767de069f64a4bdffb63956294f1c0696eb465ddb950f28ba90571709e33521b4bd75e07 +"ua-parser-js@npm:^1.0.38": + version: 1.0.38 + resolution: "ua-parser-js@npm:1.0.38" + checksum: d0772b22b027338d806ab17d1ac2896ee7485bdf9217c526028159f3cd6bb10272bb18f6196d2f94dde83e3b36dc9d2533daf08a414764f6f4f1844842383838 languageName: node linkType: hard @@ -40532,10 +41125,10 @@ __metadata: languageName: node linkType: hard -"underscore@npm:^1.13.6": - version: 1.13.6 - resolution: "underscore@npm:1.13.6" - checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 +"underscore@npm:^1.13.7": + version: 1.13.7 + resolution: "underscore@npm:1.13.7" + checksum: 174b011af29e4fbe2c70eb2baa8bfab0d0336cf2f5654f364484967bc6264a86224d0134b9176e4235c8cceae00d11839f0fd4824268de04b11c78aca1241684 languageName: node linkType: hard @@ -41040,6 +41633,16 @@ __metadata: languageName: node linkType: hard +"url@npm:^0.11.4": + version: 0.11.4 + resolution: "url@npm:0.11.4" + dependencies: + punycode: ^1.4.1 + qs: ^6.12.3 + checksum: c25e587723d343d5d4248892393bfa5039ded9c2c07095a9d005bc64b7cb8956d623c0d8da8d1a28f71986a7a8d80fc2e9f9cf84235e48fa435a5cb4451062c6 + languageName: node + linkType: hard + "use-callback-ref@npm:^1.3.0": version: 1.3.0 resolution: "use-callback-ref@npm:1.3.0" @@ -41099,14 +41702,14 @@ __metadata: languageName: node linkType: hard -"use-subscription@npm:^1.8.0": - version: 1.8.0 - resolution: "use-subscription@npm:1.8.0" +"use-subscription@npm:^1.8.2": + version: 1.8.2 + resolution: "use-subscription@npm:1.8.2" dependencies: - use-sync-external-store: ^1.2.0 + use-sync-external-store: ^1.2.2 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: beac1f0ff14fe23fd6ae9c34681258936729f343bf6532bbce36caa8f4c1019ff380783e35b4aeb7f3faaec1a83af242d7833bf7e660816d24555dbdd2c934da + checksum: 6b17f92a75405a4e6015d7762b459a43435f34d0bb9a72e512e305d6d3a61bd170e6666c3a62c2c3c7af1b7ba0b45c5a597b7eeea54c46e1cabf8ef0f971d44e languageName: node linkType: hard @@ -41128,7 +41731,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:~1.2.0, use-sync-external-store@npm:~1.2.2": +"use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:~1.2.0, use-sync-external-store@npm:~1.2.2": version: 1.2.2 resolution: "use-sync-external-store@npm:1.2.2" peerDependencies: @@ -41226,7 +41829,7 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.0, util@npm:^0.12.4": +"util@npm:^0.12.4, util@npm:^0.12.5": version: 0.12.5 resolution: "util@npm:0.12.5" dependencies: @@ -41664,9 +42267,9 @@ __metadata: languageName: node linkType: hard -"webdav@npm:^4.11.3": - version: 4.11.3 - resolution: "webdav@npm:4.11.3" +"webdav@npm:^4.11.4": + version: 4.11.4 + resolution: "webdav@npm:4.11.4" dependencies: axios: ^0.27.2 base-64: ^1.0.0 @@ -41681,7 +42284,7 @@ __metadata: path-posix: ^1.0.0 url-join: ^4.0.1 url-parse: ^1.5.10 - checksum: e5bfc66149088cd857c23a3a549650d7483dd5615cf1c4b6251a5b290a4ad8fef4975bfd99fca2d5842a0eaadc056bc0044e37893e0ad5447e6ce2e2dbd81da5 + checksum: 941a8f239d483cc283b893132675746461fe42b0932afe81ee2cad36065aeb657d722813a34966f6c6f6e95e7058453a7869e238ae1e1d1d29df9a28508dee27 languageName: node linkType: hard @@ -41772,7 +42375,7 @@ __metadata: languageName: node linkType: hard -"webpack-cli@npm:~5.0.1": +"webpack-cli@npm:~5.0.2": version: 5.0.2 resolution: "webpack-cli@npm:5.0.2" dependencies: @@ -42166,10 +42769,10 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:^3.6.19": - version: 3.6.19 - resolution: "whatwg-fetch@npm:3.6.19" - checksum: 2896bc9ca867ea514392c73e2a272f65d5c4916248fe0837a9df5b1b92f247047bc76cf7c29c28a01ac6c5fb4314021d2718958c8a08292a96d56f72b2f56806 +"whatwg-fetch@npm:^3.6.20": + version: 3.6.20 + resolution: "whatwg-fetch@npm:3.6.20" + checksum: c58851ea2c4efe5c2235f13450f426824cf0253c1d45da28f45900290ae602a20aff2ab43346f16ec58917d5562e159cd691efa368354b2e82918c2146a519c5 languageName: node linkType: hard From 2e10a451c4756f64978a0658e7ffff68a0691774 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Sat, 14 Sep 2024 09:05:11 -0300 Subject: [PATCH 13/57] ci: Include hidden files when uploading artifacts (#33286) --- .github/actions/meteor-build/action.yml | 1 + .github/workflows/ci-test-e2e.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index 525595146700..551a57d28a7c 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -133,3 +133,4 @@ runs: name: build path: /tmp/Rocket.Chat.tar.gz overwrite: true + include-hidden-files: true diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index e6c02b7b6417..3ed6f07e725d 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -293,6 +293,7 @@ jobs: with: name: playwright-test-trace-${{ matrix.mongodb-version }}-${{ matrix.shard }} path: ./apps/meteor/tests/e2e/.playwright* + include-hidden-files: true - name: Show server logs if E2E test failed if: failure() @@ -326,6 +327,7 @@ jobs: with: name: e2e-api-ee-coverage-${{ matrix.mongodb-version }}-${{ matrix.shard }} path: /tmp/coverage + include-hidden-files: true - name: Store e2e-ee-coverage if: inputs.type == 'ui' && inputs.release == 'ee' @@ -333,3 +335,4 @@ jobs: with: name: e2e-ee-coverage-${{ matrix.mongodb-version }}-${{ matrix.shard }} path: ./apps/meteor/coverage* + include-hidden-files: true From fad7f974c0fdfc5517adb389755f64eddff7c07b Mon Sep 17 00:00:00 2001 From: Rafael Tapia Date: Mon, 16 Sep 2024 10:42:54 -0300 Subject: [PATCH 14/57] feat: get contact by id endpoint (#33041) --- .../server/constant/permissions.ts | 4 ++ .../app/livechat/server/api/v1/contact.ts | 24 ++++++- .../tests/end-to-end/api/livechat/contacts.ts | 63 +++++++++++++++++++ packages/rest-typings/src/v1/omnichannel.ts | 18 ++++++ 4 files changed, 107 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/authorization/server/constant/permissions.ts b/apps/meteor/app/authorization/server/constant/permissions.ts index e5e8f7fb05dd..46d40713bad1 100644 --- a/apps/meteor/app/authorization/server/constant/permissions.ts +++ b/apps/meteor/app/authorization/server/constant/permissions.ts @@ -101,6 +101,10 @@ export const permissions = [ _id: 'update-livechat-contact', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], }, + { + _id: 'view-livechat-contact', + roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'], + }, { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, { _id: 'view-omnichannel-contact-center', diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index 7e9457d2f185..f3fec80b23fe 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -1,5 +1,9 @@ -import { LivechatCustomField, LivechatVisitors } from '@rocket.chat/models'; -import { isPOSTOmnichannelContactsProps, isPOSTUpdateOmnichannelContactsProps } from '@rocket.chat/rest-typings'; +import { LivechatContacts, LivechatCustomField, LivechatVisitors } from '@rocket.chat/models'; +import { + isPOSTOmnichannelContactsProps, + isPOSTUpdateOmnichannelContactsProps, + isGETOmnichannelContactsProps, +} from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -101,6 +105,7 @@ API.v1.addRoute( }, }, ); + API.v1.addRoute( 'omnichannel/contacts.update', { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, @@ -116,3 +121,18 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'omnichannel/contacts.get', + { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps }, + { + async get() { + if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { + throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + } + const contact = await LivechatContacts.findOneById(this.queryParams.contactId); + + return API.v1.success({ contact }); + }, + }, +); diff --git a/apps/meteor/tests/end-to-end/api/livechat/contacts.ts b/apps/meteor/tests/end-to-end/api/livechat/contacts.ts index 957d22ba92ae..c33ef255c25c 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/contacts.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/contacts.ts @@ -569,4 +569,67 @@ describe('LIVECHAT - contacts', () => { }); }); }); + + describe('[GET] omnichannel/contacts.get', () => { + let contactId: string; + const contact = { + name: faker.person.fullName(), + emails: [faker.internet.email().toLowerCase()], + phones: [faker.phone.number()], + contactManager: agentUser?._id, + }; + + before(async () => { + await updatePermission('view-livechat-contact', ['admin']); + const { body } = await request + .post(api('omnichannel/contacts')) + .set(credentials) + .send({ ...contact }); + contactId = body.contactId; + }); + + after(async () => { + await restorePermissionToRoles('view-livechat-contact'); + }); + + it('should be able get a contact by id', async () => { + const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body.contact._id).to.be.equal(contactId); + expect(res.body.contact.name).to.be.equal(contact.name); + expect(res.body.contact.emails).to.be.deep.equal(contact.emails); + expect(res.body.contact.phones).to.be.deep.equal(contact.phones); + expect(res.body.contact.contactManager).to.be.equal(contact.contactManager); + }); + + it('should return null if contact does not exist', async () => { + const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId: 'invalid' }); + + expect(res.status).to.be.equal(200); + expect(res.body).to.have.property('success', true); + expect(res.body.contact).to.be.null; + }); + + it("should return an error if user doesn't have 'view-livechat-contact' permission", async () => { + await removePermissionFromAllRoles('view-livechat-contact'); + + const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials).query({ contactId }); + + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('User does not have the permissions required for this action [error-unauthorized]'); + + await restorePermissionToRoles('view-livechat-contact'); + }); + + it('should return an error if contactId is missing', async () => { + const res = await request.get(api(`omnichannel/contacts.get`)).set(credentials); + + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error'); + expect(res.body.error).to.be.equal("must have required property 'contactId' [invalid-params]"); + expect(res.body.errorType).to.be.equal('invalid-params'); + }); + }); }); diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 1ed249f5dd55..a1c714c013b8 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -1304,6 +1304,21 @@ const POSTUpdateOmnichannelContactsSchema = { export const isPOSTUpdateOmnichannelContactsProps = ajv.compile(POSTUpdateOmnichannelContactsSchema); +type GETOmnichannelContactsProps = { contactId: string }; + +const GETOmnichannelContactsSchema = { + type: 'object', + properties: { + contactId: { + type: 'string', + }, + }, + required: ['contactId'], + additionalProperties: false, +}; + +export const isGETOmnichannelContactsProps = ajv.compile(GETOmnichannelContactsSchema); + type GETOmnichannelContactProps = { contactId: string }; const GETOmnichannelContactSchema = { @@ -3748,6 +3763,9 @@ export type OmnichannelEndpoints = { '/v1/omnichannel/contacts.update': { POST: (params: POSTUpdateOmnichannelContactsProps) => { contact: ILivechatContact }; }; + '/v1/omnichannel/contacts.get': { + GET: (params: GETOmnichannelContactsProps) => { contact: ILivechatContact | null }; + }; '/v1/omnichannel/contact.search': { GET: (params: GETOmnichannelContactSearchProps) => { contact: ILivechatVisitor | null }; From c6478fec74bd87a30e31b70b485bb5f659addde5 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 16 Sep 2024 12:20:47 -0600 Subject: [PATCH 15/57] ci: Add `ee` or `ce` to the playwright trace uploaded file (#33299) --- .github/workflows/ci-test-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 3ed6f07e725d..a80a40419e9f 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -291,7 +291,7 @@ jobs: if: inputs.type == 'ui' && always() uses: actions/upload-artifact@v4 with: - name: playwright-test-trace-${{ matrix.mongodb-version }}-${{ matrix.shard }} + name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }} path: ./apps/meteor/tests/e2e/.playwright* include-hidden-files: true From b919221b475c90789f665bcd13267a05ecd64f9c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 16 Sep 2024 14:46:24 -0600 Subject: [PATCH 16/57] fix: Federation callback not awaiting model call (#33298) --- .changeset/great-humans-live.md | 5 +++++ .../meteor/app/federation/server/hooks/afterUnsetReaction.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/great-humans-live.md diff --git a/.changeset/great-humans-live.md b/.changeset/great-humans-live.md new file mode 100644 index 000000000000..1d97d9da23ae --- /dev/null +++ b/.changeset/great-humans-live.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed a Federation callback not awaiting db call diff --git a/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js b/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js index 51181d88ab9e..995146b290bf 100644 --- a/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js +++ b/apps/meteor/app/federation/server/hooks/afterUnsetReaction.js @@ -6,7 +6,7 @@ import { getFederationDomain } from '../lib/getFederationDomain'; import { clientLogger } from '../lib/logger'; async function afterUnsetReaction(message, { user, reaction }) { - const room = Rooms.findOneById(message.rid, { fields: { federation: 1 } }); + const room = await Rooms.findOneById(message.rid, { projection: { federation: 1 } }); // If there are not federated users on this room, ignore it if (!hasExternalDomain(room)) { From 636d32d78374e943cb8b416d2172f9a6c4f68917 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 17 Sep 2024 11:06:11 -0300 Subject: [PATCH 17/57] fix: correct parameter order in afterSaveMessage to restore outgoing webhooks and related features (#33295) --- .changeset/rotten-rabbits-brush.md | 5 +++++ apps/meteor/app/autotranslate/server/autotranslate.ts | 7 ++++++- apps/meteor/app/integrations/server/triggers.ts | 7 ++++++- apps/meteor/app/irc/server/irc-bridge/index.js | 2 +- apps/meteor/ee/server/lib/engagementDashboard/startup.ts | 7 ++++++- 5 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .changeset/rotten-rabbits-brush.md diff --git a/.changeset/rotten-rabbits-brush.md b/.changeset/rotten-rabbits-brush.md new file mode 100644 index 000000000000..916f4cc8034a --- /dev/null +++ b/.changeset/rotten-rabbits-brush.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Resolves the issue where outgoing integrations failed to trigger after the version 6.12.0 upgrade by correcting the parameter order from the `afterSaveMessage` callback to listener functions. This ensures the correct room information is passed, restoring the functionality of outgoing webhooks, IRC bridge, Autotranslate, and Engagement Dashboard. diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index f3c6d9e55fdb..23e1b189a792 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -113,7 +113,12 @@ export class TranslationProviderRegistry { return; } - callbacks.add('afterSaveMessage', provider.translateMessage.bind(provider), callbacks.priority.MEDIUM, 'autotranslate'); + callbacks.add( + 'afterSaveMessage', + (message, { room }) => provider.translateMessage(message, { room }), + callbacks.priority.MEDIUM, + 'autotranslate', + ); } } diff --git a/apps/meteor/app/integrations/server/triggers.ts b/apps/meteor/app/integrations/server/triggers.ts index cdf8acda6a21..64b95827645f 100644 --- a/apps/meteor/app/integrations/server/triggers.ts +++ b/apps/meteor/app/integrations/server/triggers.ts @@ -8,7 +8,12 @@ const callbackHandler = function _callbackHandler(eventType: string) { }; }; -callbacks.add('afterSaveMessage', callbackHandler('sendMessage'), callbacks.priority.LOW, 'integrations-sendMessage'); +callbacks.add( + 'afterSaveMessage', + (message, { room }) => callbackHandler('sendMessage')(message, room), + callbacks.priority.LOW, + 'integrations-sendMessage', +); callbacks.add('afterCreateChannel', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); callbacks.add('afterCreatePrivateGroup', callbackHandler('roomCreated'), callbacks.priority.LOW, 'integrations-roomCreated'); callbacks.add('afterCreateUser', callbackHandler('userCreated'), callbacks.priority.LOW, 'integrations-userCreated'); diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index 09b7a3568362..bc5b4f0bc33f 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -209,7 +209,7 @@ class Bridge { // Chatting callbacks.add( 'afterSaveMessage', - this.onMessageReceived.bind(this, 'local', 'onSaveMessage'), + (message, { room }) => this.onMessageReceived('local', 'onSaveMessage', message, room), callbacks.priority.LOW, 'irc-on-save-message', ); diff --git a/apps/meteor/ee/server/lib/engagementDashboard/startup.ts b/apps/meteor/ee/server/lib/engagementDashboard/startup.ts index 159b121f7043..415e0323d525 100644 --- a/apps/meteor/ee/server/lib/engagementDashboard/startup.ts +++ b/apps/meteor/ee/server/lib/engagementDashboard/startup.ts @@ -3,7 +3,12 @@ import { fillFirstDaysOfMessagesIfNeeded, handleMessagesDeleted, handleMessagesS import { fillFirstDaysOfUsersIfNeeded, handleUserCreated } from './users'; export const attachCallbacks = (): void => { - callbacks.add('afterSaveMessage', handleMessagesSent, callbacks.priority.MEDIUM, 'engagementDashboard.afterSaveMessage'); + callbacks.add( + 'afterSaveMessage', + (message, { room }) => handleMessagesSent(message, { room }), + callbacks.priority.MEDIUM, + 'engagementDashboard.afterSaveMessage', + ); callbacks.add('afterDeleteMessage', handleMessagesDeleted, callbacks.priority.MEDIUM, 'engagementDashboard.afterDeleteMessage'); callbacks.add('afterCreateUser', handleUserCreated, callbacks.priority.MEDIUM, 'engagementDashboard.afterCreateUser'); }; From 3a161c43103433553442ef1d27ada9f2fd88d531 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 17 Sep 2024 10:39:21 -0600 Subject: [PATCH 18/57] feat: New endpoint for listing rooms & discussions from teams (#33177) --- .changeset/soft-mirrors-remember.md | 8 + apps/meteor/app/api/server/v1/teams.ts | 39 +++ apps/meteor/server/models/raw/Rooms.ts | 81 +++++ apps/meteor/server/services/team/service.ts | 39 ++- apps/meteor/tests/data/teams.helper.ts | 19 +- apps/meteor/tests/end-to-end/api/teams.ts | 321 ++++++++++++++++++ .../core-services/src/types/ITeamService.ts | 11 +- .../model-typings/src/models/IRoomsModel.ts | 8 + .../src/v1/teams/TeamsListChildren.ts | 36 ++ packages/rest-typings/src/v1/teams/index.ts | 6 + 10 files changed, 559 insertions(+), 9 deletions(-) create mode 100644 .changeset/soft-mirrors-remember.md create mode 100644 packages/rest-typings/src/v1/teams/TeamsListChildren.ts diff --git a/.changeset/soft-mirrors-remember.md b/.changeset/soft-mirrors-remember.md new file mode 100644 index 000000000000..78b005ee6b6e --- /dev/null +++ b/.changeset/soft-mirrors-remember.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-services": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/rest-typings": minor +--- + +New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index f64f8c820575..acb6cba2bac7 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -11,6 +11,7 @@ import { isTeamsDeleteProps, isTeamsLeaveProps, isTeamsUpdateProps, + isTeamsListChildrenProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; @@ -375,6 +376,44 @@ API.v1.addRoute( }, ); +const getTeamByIdOrNameOrParentRoom = async ( + params: { teamId: string } | { teamName: string } | { roomId: string }, +): Promise | null> => { + if ('teamId' in params && params.teamId) { + return Team.getOneById(params.teamId, { projection: { type: 1, roomId: 1 } }); + } + if ('teamName' in params && params.teamName) { + return Team.getOneByName(params.teamName, { projection: { type: 1, roomId: 1 } }); + } + if ('roomId' in params && params.roomId) { + return Team.getOneByRoomId(params.roomId, { projection: { type: 1, roomId: 1 } }); + } + return null; +}; + +// This should accept a teamId, filter (search by name on rooms collection) and sort/pagination +// should return a list of rooms/discussions from the team. the discussions will only be returned from the main room +API.v1.addRoute( + 'teams.listChildren', + { authRequired: true, validateParams: isTeamsListChildrenProps }, + { + async get() { + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort } = await this.parseJsonQuery(); + const { filter, type } = this.queryParams; + + const team = await getTeamByIdOrNameOrParentRoom(this.queryParams); + if (!team) { + return API.v1.notFound(); + } + + const data = await Team.listChildren(this.userId, team, filter, type, sort, offset, count); + + return API.v1.success({ ...data, offset, count }); + }, + }, +); + API.v1.addRoute( 'teams.members', { authRequired: true }, diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index ec3bd6fe8d40..7d4a0a54dedf 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2063,4 +2063,85 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateMany(query, update); } + + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + type?: 'channels' | 'discussions', + options?: FindOptions, + ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }> { + const nameFilter = filter ? new RegExp(escapeRegExp(filter), 'i') : undefined; + return this.col.aggregate<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>([ + { + $match: { + $and: [ + { + $or: [ + ...(!type || type === 'channels' ? [{ teamId }] : []), + ...(!type || type === 'discussions' ? [{ prid: teamRoomId }] : []), + ], + }, + ...(nameFilter ? [{ $or: [{ fname: nameFilter }, { name: nameFilter }] }] : []), + ], + }, + }, + { + $lookup: { + from: 'rocketchat_subscription', + let: { + roomId: '$_id', + }, + pipeline: [ + { + $match: { + $and: [ + { + $expr: { + $eq: ['$rid', '$$roomId'], + }, + }, + { + $expr: { + $eq: ['$u._id', userId], + }, + }, + { + $expr: { + $ne: ['$t', 'c'], + }, + }, + ], + }, + }, + { + $project: { _id: 1 }, + }, + ], + as: 'subscription', + }, + }, + { + $match: { + $or: [ + { t: 'c' }, + { + $expr: { + $ne: [{ $size: '$subscription' }, 0], + }, + }, + ], + }, + }, + { $project: { subscription: 0 } }, + { $sort: options?.sort || { ts: 1 } }, + { + $facet: { + totalCount: [{ $count: 'count' }], + paginatedResults: [{ $skip: options?.skip || 0 }, { $limit: options?.limit || 50 }], + }, + }, + ]); + } } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 27f7af1f1b1c..f5218c88402a 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -913,8 +913,8 @@ export class TeamService extends ServiceClassInternal implements ITeamService { }); } - async getOneByRoomId(roomId: string): Promise { - const room = await Rooms.findOneById(roomId); + async getOneByRoomId(roomId: string, options?: FindOptions): Promise { + const room = await Rooms.findOneById(roomId, { projection: { teamId: 1 } }); if (!room) { throw new Error('invalid-room'); @@ -924,7 +924,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('room-not-on-team'); } - return Team.findOneById(room.teamId); + return Team.findOneById(room.teamId, options); } async addRolesToMember(teamId: string, userId: string, roles: Array): Promise { @@ -1078,4 +1078,37 @@ export class TeamService extends ServiceClassInternal implements ITeamService { const parentRoom = await this.getParentRoom(team); return { team, ...(parentRoom && { parentRoom }) }; } + + // Returns the list of rooms and discussions a user has access to inside a team + // Rooms returned are a composition of the rooms the user is in + public rooms + discussions from the main room (if any) + async listChildren( + userId: string, + team: AtLeast, + filter?: string, + type?: 'channels' | 'discussions', + sort?: Record, + skip = 0, + limit = 10, + ): Promise<{ total: number; data: IRoom[] }> { + const mainRoom = await Rooms.findOneById(team.roomId, { projection: { _id: 1 } }); + if (!mainRoom) { + throw new Error('error-invalid-team-no-main-room'); + } + + const isMember = await TeamMember.findOneByUserIdAndTeamId(userId, team._id, { + projection: { _id: 1 }, + }); + + if (!isMember) { + throw new Error('error-invalid-team-not-a-member'); + } + + const [{ totalCount: [{ count: total }] = [], paginatedResults: data = [] }] = + (await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, type, { skip, limit, sort }).toArray()) || []; + + return { + total, + data, + }; + } } diff --git a/apps/meteor/tests/data/teams.helper.ts b/apps/meteor/tests/data/teams.helper.ts index 8fc60bd19fd4..f6cba25f86c9 100644 --- a/apps/meteor/tests/data/teams.helper.ts +++ b/apps/meteor/tests/data/teams.helper.ts @@ -2,11 +2,20 @@ import type { ITeam, TEAM_TYPE } from '@rocket.chat/core-typings'; import { api, request } from './api-data'; -export const createTeam = async (credentials: Record, teamName: string, type: TEAM_TYPE): Promise => { - const response = await request.post(api('teams.create')).set(credentials).send({ - name: teamName, - type, - }); +export const createTeam = async ( + credentials: Record, + teamName: string, + type: TEAM_TYPE, + members?: string[], +): Promise => { + const response = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: teamName, + type, + ...(members && { members }), + }); return response.body.team; }; diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index ca07d3e32679..b630a97b1727 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2217,4 +2217,325 @@ describe('[Teams]', () => { }); }); }); + + describe('[teams.listChildren]', () => { + const teamName = `team-${Date.now()}`; + let testTeam: ITeam; + let testPrivateTeam: ITeam; + let testUser: IUser; + let testUserCredentials: Credentials; + + let privateRoom: IRoom; + let privateRoom2: IRoom; + let publicRoom: IRoom; + let publicRoom2: IRoom; + + let discussionOnPrivateRoom: IRoom; + let discussionOnPublicRoom: IRoom; + let discussionOnMainRoom: IRoom; + + before('Create test team', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + + const members = testUser.username ? [testUser.username] : []; + testTeam = await createTeam(credentials, teamName, 0, members); + testPrivateTeam = await createTeam(testUserCredentials, `${teamName}private`, 1, []); + }); + + before('make user owner', async () => { + await request + .post(api('teams.updateMember')) + .set(credentials) + .send({ + teamName: testTeam.name, + member: { + userId: testUser._id, + roles: ['member', 'owner'], + }, + }) + .expect('Content-Type', 'application/json') + .expect(200); + }); + + before('create rooms', async () => { + privateRoom = (await createRoom({ type: 'p', name: `test-p-${Date.now()}` })).body.group; + privateRoom2 = (await createRoom({ type: 'p', name: `test-p2-${Date.now()}`, credentials: testUserCredentials })).body.group; + publicRoom = (await createRoom({ type: 'c', name: `test-c-${Date.now()}` })).body.channel; + publicRoom2 = (await createRoom({ type: 'c', name: `test-c2-${Date.now()}` })).body.channel; + + await Promise.all([ + request + .post(api('teams.addRooms')) + .set(credentials) + .send({ + rooms: [privateRoom._id, publicRoom._id, publicRoom2._id], + teamId: testTeam._id, + }) + .expect(200), + request + .post(api('teams.addRooms')) + .set(testUserCredentials) + .send({ + rooms: [privateRoom2._id], + teamId: testTeam._id, + }) + .expect(200), + ]); + }); + + before('Create discussions', async () => { + discussionOnPrivateRoom = ( + await request + .post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: privateRoom._id, + t_name: `test-d-${Date.now()}`, + }) + ).body.discussion; + discussionOnPublicRoom = ( + await request + .post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: publicRoom._id, + t_name: `test-d-${Date.now()}`, + }) + ).body.discussion; + discussionOnMainRoom = ( + await request + .post(api('rooms.createDiscussion')) + .set(credentials) + .send({ + prid: testTeam.roomId, + t_name: `test-d-${Date.now()}`, + }) + ).body.discussion; + }); + + after(async () => { + await Promise.all([ + deleteRoom({ type: 'p', roomId: privateRoom._id }), + deleteRoom({ type: 'p', roomId: privateRoom2._id }), + deleteRoom({ type: 'c', roomId: publicRoom._id }), + deleteRoom({ type: 'c', roomId: publicRoom2._id }), + deleteRoom({ type: 'p', roomId: discussionOnPrivateRoom._id }), + deleteRoom({ type: 'c', roomId: discussionOnPublicRoom._id }), + deleteRoom({ type: 'c', roomId: discussionOnMainRoom._id }), + deleteTeam(credentials, teamName), + deleteTeam(credentials, testPrivateTeam.name), + deleteUser({ _id: testUser._id }), + ]); + }); + + it('should fail if user is not logged in', async () => { + await request.get(api('teams.listChildren')).expect(401); + }); + + it('should fail if teamId is not passed as queryparam', async () => { + await request.get(api('teams.listChildren')).set(credentials).expect(400); + }); + + it('should fail if teamId is not valid', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamId: 'invalid' }).expect(404); + }); + + it('should fail if teamId is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamId: '' }).expect(404); + }); + + it('should fail if both properties are passed', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamId: testTeam._id, teamName: testTeam.name }).expect(400); + }); + + it('should fail if teamName is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamName: '' }).expect(404); + }); + + it('should fail if teamName is invalid', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamName: 'invalid' }).expect(404); + }); + + it('should fail if roomId is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ roomId: '' }).expect(404); + }); + + it('should fail if roomId is invalid', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ teamName: 'invalid' }).expect(404); + }); + + it('should return a list of valid rooms for user', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testTeam._id }).set(credentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.an('object'); + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.undefined; + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a valid list of rooms for non admin member too', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamName: testTeam.name }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.undefined; + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.an('object'); + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a valid list of rooms for non admin member too when filtering by teams main room id', async () => { + const res = await request.get(api('teams.listChildren')).query({ roomId: testTeam.roomId }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.undefined; + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.an('object'); + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a list of rooms filtered by name using the filter parameter', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, filter: 'test-p' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(1); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data[0]._id).to.be.equal(privateRoom._id); + expect(res.body.data.find((room: IRoom) => room._id === privateRoom2._id)).to.be.undefined; + }); + + it('should paginate results', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, offset: 1, count: 2 }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(2); + }); + + it('should return only items of type channel', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'channels' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(4); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data.some((room: IRoom) => !!room.prid)).to.be.false; + }); + + it('should return only items of type discussion', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'discussions' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(1); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(1); + expect(res.body.data.every((room: IRoom) => !!room.prid)).to.be.true; + }); + + it('should return both when type is not passed', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testTeam._id }).set(credentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + expect(res.body.data.some((room: IRoom) => !!room.prid)).to.be.true; + expect(res.body.data.some((room: IRoom) => !room.prid)).to.be.true; + }); + + it('should fail if type is other than channel or discussion', async () => { + await request.get(api('teams.listChildren')).query({ teamId: testTeam._id, type: 'other' }).set(credentials).expect(400); + }); + + it('should properly list children of a private team', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testPrivateTeam._id }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(1); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(1); + }); + + it('should throw an error when a non member user tries to fetch info for team', async () => { + await request.get(api('teams.listChildren')).query({ teamId: testPrivateTeam._id }).set(credentials).expect(400); + }); + }); }); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 3caa6a2e97df..132df89470ca 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -112,7 +112,7 @@ export interface ITeamService { getOneById

(teamId: string, options?: FindOptions

): Promise; getOneByName(teamName: string | RegExp, options?: FindOptions): Promise; getOneByMainRoomId(teamId: string): Promise | null>; - getOneByRoomId(teamId: string): Promise; + getOneByRoomId(teamId: string, options?: FindOptions): Promise; getMatchingTeamRooms(teamId: string, rids: Array): Promise>; autocomplete(uid: string, name: string): Promise; getAllPublicTeams(options?: FindOptions): Promise>; @@ -129,4 +129,13 @@ export interface ITeamService { getRoomInfo( room: AtLeast, ): Promise<{ team?: Pick; parentRoom?: Pick }>; + listChildren( + userId: string, + team: AtLeast, + filter?: string, + type?: 'channels' | 'discussions', + sort?: Record, + skip?: number, + limit?: number, + ): Promise<{ total: number; data: IRoom[] }>; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 9097a89c1413..91802f836719 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -282,4 +282,12 @@ export interface IRoomsModel extends IBaseModel { getSubscribedRoomIdsWithoutE2EKeys(uid: IUser['_id']): Promise; removeUsersFromE2EEQueueByRoomId(roomId: IRoom['_id'], uids: IUser['_id'][]): Promise; removeUserFromE2EEQueue(uid: IUser['_id']): Promise; + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + type?: 'channels' | 'discussions', + options?: FindOptions, + ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>; } diff --git a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts new file mode 100644 index 000000000000..41128e18a05f --- /dev/null +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -0,0 +1,36 @@ +import type { ITeam } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ajv } from '../Ajv'; + +type GeneralProps = { + filter?: string; + type?: 'channels' | 'discussions'; +}; + +export type TeamsListChildrenProps = + | PaginatedRequest< + { + teamId: ITeam['_id']; + } & GeneralProps + > + | PaginatedRequest<{ teamName: ITeam['name'] } & GeneralProps> + | PaginatedRequest<{ roomId: ITeam['roomId'] } & GeneralProps>; + +const TeamsListChildrenPropsSchema = { + type: 'object', + properties: { + teamId: { type: 'string' }, + teamName: { type: 'string' }, + type: { type: 'string', enum: ['channels', 'discussions'] }, + roomId: { type: 'string' }, + filter: { type: 'string' }, + offset: { type: 'number' }, + count: { type: 'number' }, + sort: { type: 'string' }, + }, + additionalProperties: false, + oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }, { required: ['roomId'] }], +}; + +export const isTeamsListChildrenProps = ajv.compile(TeamsListChildrenPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/index.ts b/packages/rest-typings/src/v1/teams/index.ts index d63e6da8bd8a..a4ae6c7bca0f 100644 --- a/packages/rest-typings/src/v1/teams/index.ts +++ b/packages/rest-typings/src/v1/teams/index.ts @@ -6,6 +6,7 @@ import type { TeamsAddMembersProps } from './TeamsAddMembersProps'; import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; import type { TeamsDeleteProps } from './TeamsDeleteProps'; import type { TeamsLeaveProps } from './TeamsLeaveProps'; +import type { TeamsListChildrenProps } from './TeamsListChildren'; import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; @@ -19,6 +20,7 @@ export * from './TeamsRemoveMemberProps'; export * from './TeamsRemoveRoomProps'; export * from './TeamsUpdateMemberProps'; export * from './TeamsUpdateProps'; +export * from './TeamsListChildren'; type ITeamAutocompleteResult = Pick; @@ -184,4 +186,8 @@ export type TeamsEndpoints = { room: IRoom; }; }; + + '/v1/teams.listChildren': { + GET: (params: TeamsListChildrenProps) => PaginatedResult<{ data: IRoom[] }>; + }; }; From f27092b94d5e4070a4eb74ce49b6b4eec191446e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 17 Sep 2024 13:30:43 -0600 Subject: [PATCH 19/57] chore: Update typings on callbacks to accept less than a full room object (#33305) --- apps/meteor/app/lib/server/functions/isTheLastMessage.ts | 4 ++-- .../server/services/messages/hooks/BeforeFederationActions.ts | 3 ++- apps/meteor/server/services/messages/service.ts | 4 ++-- packages/core-services/src/types/IMessageService.ts | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/isTheLastMessage.ts b/apps/meteor/app/lib/server/functions/isTheLastMessage.ts index f8e5be94002c..f1f1fb4c1497 100644 --- a/apps/meteor/app/lib/server/functions/isTheLastMessage.ts +++ b/apps/meteor/app/lib/server/functions/isTheLastMessage.ts @@ -1,7 +1,7 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, AtLeast } from '@rocket.chat/core-typings'; import { settings } from '../../../settings/server'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const isTheLastMessage = (room: IRoom, message: Pick) => +export const isTheLastMessage = (room: AtLeast, message: Pick) => settings.get('Store_Last_Message') && (!room.lastMessage || room.lastMessage._id === message._id); diff --git a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts index a954e4899970..19f42626c216 100644 --- a/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts +++ b/apps/meteor/server/services/messages/hooks/BeforeFederationActions.ts @@ -1,9 +1,10 @@ +import type { AtLeast } from '@rocket.chat/core-typings'; import { type IMessage, type IRoom, isMessageFromMatrixFederation, isRoomFederated } from '@rocket.chat/core-typings'; import { isFederationEnabled, isFederationReady } from '../../federation/utils'; export class FederationActions { - public static shouldPerformAction(message: IMessage, room: IRoom): boolean { + public static shouldPerformAction(message: IMessage, room: AtLeast): boolean { if (isMessageFromMatrixFederation(message) || isRoomFederated(room)) { return isFederationEnabled() && isFederationReady(); } diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index b20b5236b7fe..85afcf394f28 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -1,6 +1,6 @@ import type { IMessageService } from '@rocket.chat/core-services'; import { Authorization, ServiceClassInternal } from '@rocket.chat/core-services'; -import { type IMessage, type MessageTypesValues, type IUser, type IRoom, isEditedMessage } from '@rocket.chat/core-typings'; +import { type IMessage, type MessageTypesValues, type IUser, type IRoom, isEditedMessage, type AtLeast } from '@rocket.chat/core-typings'; import { Messages, Rooms } from '@rocket.chat/models'; import { deleteMessage } from '../../../app/lib/server/functions/deleteMessage'; @@ -244,7 +244,7 @@ export class MessageService extends ServiceClassInternal implements IMessageServ // await Room.join({ room, user }); // } - async beforeReacted(message: IMessage, room: IRoom) { + async beforeReacted(message: IMessage, room: AtLeast) { if (!FederationActions.shouldPerformAction(message, room)) { throw new FederationMatrixInvalidConfigurationError('Unable to react to message'); } diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index ca84f78ea677..29da139ef63c 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -1,4 +1,4 @@ -import type { IMessage, MessageTypesValues, IUser, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, MessageTypesValues, IUser, IRoom, AtLeast } from '@rocket.chat/core-typings'; export interface IMessageService { sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise; @@ -21,6 +21,6 @@ export interface IMessageService { deleteMessage(user: IUser, message: IMessage): Promise; updateMessage(message: IMessage, user: IUser, originalMsg?: IMessage): Promise; reactToMessage(userId: string, reaction: string, messageId: IMessage['_id'], shouldReact?: boolean): Promise; - beforeReacted(message: IMessage, room: IRoom): Promise; + beforeReacted(message: IMessage, room: AtLeast): Promise; beforeDelete(message: IMessage, room: IRoom): Promise; } From 4202d6570cfa870cd196a5b10bc29041fbff50eb Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 17 Sep 2024 17:56:33 -0300 Subject: [PATCH 20/57] fix: resolve avatar download issue on setUsername by refining service selection logic (#33193) --- .changeset/small-crabs-travel.md | 5 + .../app/lib/server/functions/setUsername.ts | 23 +- .../lib/server/functions/setUsername.spec.ts | 271 ++++++++++++++++++ 3 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 .changeset/small-crabs-travel.md create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts diff --git a/.changeset/small-crabs-travel.md b/.changeset/small-crabs-travel.md new file mode 100644 index 000000000000..201494a5b70f --- /dev/null +++ b/.changeset/small-crabs-travel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed avatar blob image setting in setUserAvatar method by correcting service handling logic. diff --git a/apps/meteor/app/lib/server/functions/setUsername.ts b/apps/meteor/app/lib/server/functions/setUsername.ts index 5b2b1923da75..c4d2c47c6d9d 100644 --- a/apps/meteor/app/lib/server/functions/setUsername.ts +++ b/apps/meteor/app/lib/server/functions/setUsername.ts @@ -102,23 +102,22 @@ export const _setUsername = async function (userId: string, u: string, fullUser: // Set new username* await Users.setUsername(user._id, username); user.username = username; + if (!previousUsername && settings.get('Accounts_SetDefaultAvatar') === true) { - // eslint-disable-next-line @typescript-eslint/ban-types - const avatarSuggestions = (await getAvatarSuggestionForUser(user)) as {}; - let gravatar; - for await (const service of Object.keys(avatarSuggestions)) { - const avatarData = avatarSuggestions[+service as keyof typeof avatarSuggestions]; + const avatarSuggestions = await getAvatarSuggestionForUser(user); + let avatarData; + let serviceName = 'gravatar'; + + for (const service of Object.keys(avatarSuggestions)) { + avatarData = avatarSuggestions[service]; if (service !== 'gravatar') { - // eslint-disable-next-line dot-notation - await setUserAvatar(user, avatarData['blob'], avatarData['contentType'], service); - gravatar = null; + serviceName = service; break; } - gravatar = avatarData; } - if (gravatar != null) { - // eslint-disable-next-line dot-notation - await setUserAvatar(user, gravatar['blob'], gravatar['contentType'], 'gravatar'); + + if (avatarData) { + await setUserAvatar(user, avatarData.blob, avatarData.contentType, serviceName); } } diff --git a/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts b/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts new file mode 100644 index 000000000000..c6b6f9a26fae --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/setUsername.spec.ts @@ -0,0 +1,271 @@ +import { expect } from 'chai'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +describe('setUsername', () => { + const userId = 'userId'; + const username = 'validUsername'; + + const stubs = { + Users: { + findOneById: sinon.stub(), + setUsername: sinon.stub(), + }, + Accounts: { + sendEnrollmentEmail: sinon.stub(), + }, + settings: { + get: sinon.stub(), + }, + api: { + broadcast: sinon.stub(), + }, + Invites: { + findOneById: sinon.stub(), + }, + callbacks: { + run: sinon.stub(), + }, + checkUsernameAvailability: sinon.stub(), + validateUsername: sinon.stub(), + saveUserIdentity: sinon.stub(), + joinDefaultChannels: sinon.stub(), + getAvatarSuggestionForUser: sinon.stub(), + setUserAvatar: sinon.stub(), + addUserToRoom: sinon.stub(), + notifyOnUserChange: sinon.stub(), + RateLimiter: { + limitFunction: sinon.stub(), + }, + underscore: { + escape: sinon.stub(), + }, + SystemLogger: sinon.stub(), + }; + + const { setUsernameWithValidation, _setUsername } = proxyquire + .noCallThru() + .load('../../../../../../app/lib/server/functions/setUsername', { + 'meteor/meteor': { Meteor: { Error } }, + '@rocket.chat/core-services': { api: stubs.api }, + '@rocket.chat/models': { Users: stubs.Users, Invites: stubs.Invites }, + 'meteor/accounts-base': { Accounts: stubs.Accounts }, + 'underscore': stubs.underscore, + '../../../settings/server': { settings: stubs.settings }, + '../lib': { notifyOnUserChange: stubs.notifyOnUserChange, RateLimiter: stubs.RateLimiter }, + './addUserToRoom': { addUserToRoom: stubs.addUserToRoom }, + './checkUsernameAvailability': { checkUsernameAvailability: stubs.checkUsernameAvailability }, + './getAvatarSuggestionForUser': { getAvatarSuggestionForUser: stubs.getAvatarSuggestionForUser }, + './joinDefaultChannels': { joinDefaultChannels: stubs.joinDefaultChannels }, + './saveUserIdentity': { saveUserIdentity: stubs.saveUserIdentity }, + './setUserAvatar': { setUserAvatar: stubs.setUserAvatar }, + './validateUsername': { validateUsername: stubs.validateUsername }, + '../../../../lib/callbacks': { callbacks: stubs.callbacks }, + '../../../../server/lib/logger/system': { SystemLogger: stubs.SystemLogger }, + }); + + afterEach(() => { + stubs.Users.findOneById.reset(); + stubs.Users.setUsername.reset(); + stubs.Accounts.sendEnrollmentEmail.reset(); + stubs.settings.get.reset(); + stubs.api.broadcast.reset(); + stubs.Invites.findOneById.reset(); + stubs.callbacks.run.reset(); + stubs.checkUsernameAvailability.reset(); + stubs.validateUsername.reset(); + stubs.saveUserIdentity.reset(); + stubs.joinDefaultChannels.reset(); + stubs.getAvatarSuggestionForUser.reset(); + stubs.setUserAvatar.reset(); + stubs.addUserToRoom.reset(); + stubs.notifyOnUserChange.reset(); + stubs.RateLimiter.limitFunction.reset(); + stubs.underscore.escape.reset(); + stubs.SystemLogger.reset(); + }); + + describe('setUsernameWithValidation', () => { + it('should throw an error if username is invalid', async () => { + try { + await setUsernameWithValidation(userId, ''); + } catch (error: any) { + expect(error.message).to.equal('error-invalid-username'); + } + }); + + it('should throw an error if user is not found', async () => { + stubs.Users.findOneById.withArgs(userId).returns(null); + + try { + await setUsernameWithValidation(userId, username); + } catch (error: any) { + expect(stubs.Users.findOneById.calledOnce).to.be.true; + expect(error.message).to.equal('error-invalid-user'); + } + }); + + it('should throw an error if username change is not allowed', async () => { + stubs.Users.findOneById.resolves({ username: 'oldUsername' }); + stubs.settings.get.withArgs('Accounts_AllowUsernameChange').returns(false); + + try { + await setUsernameWithValidation(userId, username); + } catch (error: any) { + expect(stubs.settings.get.calledOnce).to.be.true; + expect(error.message).to.equal('error-not-allowed'); + } + }); + + it('should throw an error if username is not valid', async () => { + stubs.Users.findOneById.resolves({ username: null }); + stubs.validateUsername.returns(false); + + try { + await setUsernameWithValidation(userId, 'invalid-username'); + } catch (error: any) { + expect(stubs.validateUsername.calledOnce).to.be.true; + expect(error.message).to.equal('username-invalid'); + } + }); + + it('should throw an error if username is already in use', async () => { + stubs.Users.findOneById.resolves({ username: null }); + stubs.validateUsername.returns(true); + stubs.checkUsernameAvailability.resolves(false); + + try { + await setUsernameWithValidation(userId, 'existingUsername'); + } catch (error: any) { + expect(stubs.checkUsernameAvailability.calledOnce).to.be.true; + expect(error.message).to.equal('error-field-unavailable'); + } + }); + + it('should save the user identity when valid username is set', async () => { + stubs.Users.findOneById.resolves({ _id: userId, username: null }); + stubs.settings.get.withArgs('Accounts_AllowUsernameChange').returns(true); + stubs.validateUsername.returns(true); + stubs.checkUsernameAvailability.resolves(true); + stubs.saveUserIdentity.resolves(true); + + await setUsernameWithValidation(userId, 'newUsername'); + + expect(stubs.saveUserIdentity.calledOnce).to.be.true; + expect(stubs.joinDefaultChannels.calledOnceWith(userId, undefined)).to.be.true; + }); + }); + + describe('_setUsername', () => { + it('should return false if userId or username is missing', async () => { + const result = await _setUsername(null, '', {}); + expect(result).to.be.false; + }); + + it('should return false if username is invalid', async () => { + stubs.validateUsername.returns(false); + + const result = await _setUsername(userId, 'invalid-username', {}); + expect(result).to.be.false; + }); + + it('should return user if username is already set', async () => { + stubs.validateUsername.returns(true); + const mockUser = { username }; + + const result = await _setUsername(userId, username, mockUser); + expect(result).to.equal(mockUser); + }); + + it('should set username when user has no previous username', async () => { + const mockUser = { _id: userId, emails: [{ address: 'test@example.com' }] }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + + await _setUsername(userId, username, mockUser); + + expect(stubs.Users.setUsername.calledOnceWith(userId, username)); + expect(stubs.checkUsernameAvailability.calledOnceWith(username)); + expect(stubs.api.broadcast.calledOnceWith('user.autoupdate', { user: mockUser })); + }); + + it('should set username when user has and old that is different from new', async () => { + const mockUser = { _id: userId, username: 'oldUsername', emails: [{ address: 'test@example.com' }] }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + + await _setUsername(userId, username, mockUser); + + expect(stubs.Users.setUsername.calledOnceWith(userId, username)); + expect(stubs.checkUsernameAvailability.calledOnceWith(username)); + expect(stubs.api.broadcast.calledOnceWith('user.autoupdate', { user: mockUser })); + }); + + it('should set username when user has and old that is different from new', async () => { + const mockUser = { _id: userId, username: 'oldUsername', emails: [{ address: 'test@example.com' }] }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + + await _setUsername(userId, username, mockUser); + + expect(stubs.Users.setUsername.calledOnceWith(userId, username)); + expect(stubs.checkUsernameAvailability.calledOnceWith(username)); + expect(stubs.api.broadcast.calledOnceWith('user.autoupdate', { user: mockUser })); + }); + + it('should set avatar if Accounts_SetDefaultAvatar is enabled', async () => { + const mockUser = { _id: userId, username: null }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(true); + stubs.getAvatarSuggestionForUser.resolves({ gravatar: { blob: 'blobData', contentType: 'image/png' } }); + + await _setUsername(userId, username, mockUser); + + expect(stubs.setUserAvatar.calledOnceWith(mockUser, 'blobData', 'image/png', 'gravatar')).to.be.true; + }); + + it('should not set avatar if Accounts_SetDefaultAvatar is disabled', async () => { + const mockUser = { _id: userId, username: null }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(false); + + await _setUsername(userId, username, mockUser); + + expect(stubs.setUserAvatar.called).to.be.false; + }); + + it('should not set avatar if no avatar suggestions are available', async () => { + const mockUser = { _id: userId, username: null }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(true); + stubs.getAvatarSuggestionForUser.resolves({}); + + await _setUsername(userId, username, mockUser); + + expect(stubs.setUserAvatar.called).to.be.false; + }); + + it('should add user to room if inviteToken is present', async () => { + const mockUser = { _id: userId, username: null, inviteToken: 'invite token' }; + stubs.validateUsername.returns(true); + stubs.Users.findOneById.resolves(mockUser); + stubs.checkUsernameAvailability.resolves(true); + stubs.settings.get.withArgs('Accounts_SetDefaultAvatar').returns(true); + stubs.getAvatarSuggestionForUser.resolves({ gravatar: { blob: 'blobData', contentType: 'image/png' } }); + stubs.Invites.findOneById.resolves({ rid: 'room id' }); + + await _setUsername(userId, username, mockUser); + + expect(stubs.addUserToRoom.calledOnceWith('room id', mockUser)).to.be.true; + }); + }); +}); From 9a38c8e13f925d88ece6955333773bce45ba7536 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Wed, 18 Sep 2024 09:31:32 -0300 Subject: [PATCH 21/57] feat: Allow managing association to business units on departments' creation and update (#32682) --- .changeset/dirty-stingrays-beg.md | 7 + .../imports/server/rest/departments.ts | 15 +- .../app/livechat/server/lib/LivechatTyped.ts | 25 +- .../livechat/server/methods/saveDepartment.ts | 5 +- .../livechat-enterprise/server/hooks/index.ts | 1 + .../server/hooks/manageDepartmentUnit.ts | 53 ++ .../ee/server/models/raw/LivechatUnit.ts | 29 +- .../server/hooks/manageDepartmentUnit.spec.ts | 183 ++++++ apps/meteor/lib/callbacks.ts | 2 + .../server/models/raw/LivechatDepartment.ts | 12 + apps/meteor/tests/data/livechat/department.ts | 40 +- apps/meteor/tests/data/livechat/rooms.ts | 47 +- apps/meteor/tests/data/livechat/units.ts | 38 +- .../tests/end-to-end/api/livechat/14-units.ts | 556 +++++++++++++++++- .../core-typings/src/ILivechatDepartment.ts | 1 + .../src/models/ILivechatDepartmentModel.ts | 3 + .../src/models/ILivechatUnitModel.ts | 2 + packages/rest-typings/src/v1/omnichannel.ts | 12 +- 18 files changed, 982 insertions(+), 49 deletions(-) create mode 100644 .changeset/dirty-stingrays-beg.md create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts create mode 100644 apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts diff --git a/.changeset/dirty-stingrays-beg.md b/.changeset/dirty-stingrays-beg.md new file mode 100644 index 000000000000..cf5e3a4ca839 --- /dev/null +++ b/.changeset/dirty-stingrays-beg.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/rest-typings": minor +--- + +Added support for specifying a unit on departments' creation and update diff --git a/apps/meteor/app/livechat/imports/server/rest/departments.ts b/apps/meteor/app/livechat/imports/server/rest/departments.ts index 252a83855700..e56feeac2fa3 100644 --- a/apps/meteor/app/livechat/imports/server/rest/departments.ts +++ b/apps/meteor/app/livechat/imports/server/rest/departments.ts @@ -57,10 +57,18 @@ API.v1.addRoute( check(this.bodyParams, { department: Object, agents: Match.Maybe(Array), + departmentUnit: Match.Maybe({ _id: Match.Optional(String) }), }); const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {}; - const department = await LivechatTs.saveDepartment(null, this.bodyParams.department as ILivechatDepartment, agents); + const { departmentUnit } = this.bodyParams; + const department = await LivechatTs.saveDepartment( + this.userId, + null, + this.bodyParams.department as ILivechatDepartment, + agents, + departmentUnit || {}, + ); if (department) { return API.v1.success({ @@ -112,17 +120,18 @@ API.v1.addRoute( check(this.bodyParams, { department: Object, agents: Match.Maybe(Array), + departmentUnit: Match.Maybe({ _id: Match.Optional(String) }), }); const { _id } = this.urlParams; - const { department, agents } = this.bodyParams; + const { department, agents, departmentUnit } = this.bodyParams; if (!permissionToSave) { throw new Error('error-not-allowed'); } const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {}; - await LivechatTs.saveDepartment(_id, department, agentParam); + await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {}); return API.v1.success({ department: await LivechatDepartment.findOneById(_id), diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 89d125033977..ade6726336ec 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -1789,18 +1789,37 @@ class LivechatClass { * @param {string|null} _id - The department id * @param {Partial} departmentData * @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents + * @param {{_id?: string}} [departmentUnit] - The department's unit id */ async saveDepartment( + userId: string, _id: string | null, departmentData: LivechatDepartmentDTO, departmentAgents?: { upsert?: { agentId: string; count?: number; order?: number }[]; remove?: { agentId: string; count?: number; order?: number }; }, + departmentUnit?: { _id?: string }, ) { check(_id, Match.Maybe(String)); + if (departmentUnit?._id !== undefined && typeof departmentUnit._id !== 'string') { + throw new Meteor.Error('error-invalid-department-unit', 'Invalid department unit id provided', { + method: 'livechat:saveDepartment', + }); + } - const department = _id ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null; + const department = _id + ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1, parentId: 1 } }) + : null; + + if (departmentUnit && !departmentUnit._id && department && department.parentId) { + const isLastDepartmentInUnit = (await LivechatDepartment.countDepartmentsInUnit(department.parentId)) === 1; + if (isLastDepartmentInUnit) { + throw new Meteor.Error('error-unit-cant-be-empty', "The last department in a unit can't be removed", { + method: 'livechat:saveDepartment', + }); + } + } if (!department && !(await isDepartmentCreationAvailable())) { throw new Meteor.Error('error-max-departments-number-reached', 'Maximum number of departments reached', { @@ -1887,6 +1906,10 @@ class LivechatClass { await callbacks.run('livechat.afterDepartmentDisabled', departmentDB); } + if (departmentUnit) { + await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id }); + } + return departmentDB; } } diff --git a/apps/meteor/app/livechat/server/methods/saveDepartment.ts b/apps/meteor/app/livechat/server/methods/saveDepartment.ts index b4833523ab3f..659f85f49945 100644 --- a/apps/meteor/app/livechat/server/methods/saveDepartment.ts +++ b/apps/meteor/app/livechat/server/methods/saveDepartment.ts @@ -30,12 +30,13 @@ declare module '@rocket.chat/ddp-client' { order?: number | undefined; }[] | undefined, + departmentUnit?: { _id?: string }, ) => ILivechatDepartment; } } Meteor.methods({ - async 'livechat:saveDepartment'(_id, departmentData, departmentAgents) { + async 'livechat:saveDepartment'(_id, departmentData, departmentAgents, departmentUnit) { const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-departments'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -43,6 +44,6 @@ Meteor.methods({ }); } - return Livechat.saveDepartment(_id, departmentData, { upsert: departmentAgents }); + return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit); }, }); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts index c5bf0a5aa392..a4b66087be2e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts @@ -26,3 +26,4 @@ import './afterInquiryQueued'; import './sendPdfTranscriptOnClose'; import './applyRoomRestrictions'; import './afterTagRemoved'; +import './manageDepartmentUnit'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts new file mode 100644 index 000000000000..7de7ef0d6bf6 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts @@ -0,0 +1,53 @@ +import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models'; + +import { hasAnyRoleAsync } from '../../../../../app/authorization/server/functions/hasRole'; +import { callbacks } from '../../../../../lib/callbacks'; +import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles'; + +export const manageDepartmentUnit = async ({ userId, departmentId, unitId }: { userId: string; departmentId: string; unitId: string }) => { + const accessibleUnits = await getUnitsFromUser(userId); + const isLivechatManager = await hasAnyRoleAsync(userId, ['admin', 'livechat-manager']); + const department = await LivechatDepartment.findOneById>(departmentId, { + projection: { ancestors: 1, parentId: 1 }, + }); + + const isDepartmentAlreadyInUnit = unitId && department?.ancestors?.includes(unitId); + if (!department || isDepartmentAlreadyInUnit) { + return; + } + + const currentDepartmentUnitId = department.parentId; + const canManageNewUnit = !unitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(unitId)); + const canManageCurrentUnit = + !currentDepartmentUnitId || isLivechatManager || (Array.isArray(accessibleUnits) && accessibleUnits.includes(currentDepartmentUnitId)); + if (!canManageNewUnit || !canManageCurrentUnit) { + return; + } + + if (unitId) { + const unit = await LivechatUnit.findOneById>(unitId, { + projection: { ancestors: 1 }, + }); + + if (!unit) { + return; + } + + if (currentDepartmentUnitId) { + await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId); + } + + await LivechatDepartment.addDepartmentToUnit(departmentId, unitId, [unitId, ...(unit.ancestors || [])]); + await LivechatUnit.incrementDepartmentsCount(unitId); + return; + } + + if (currentDepartmentUnitId) { + await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId); + } + + await LivechatDepartment.removeDepartmentFromUnit(departmentId); +}; + +callbacks.add('livechat.manageDepartmentUnit', manageDepartmentUnit, callbacks.priority.HIGH, 'livechat-manage-department-unit'); diff --git a/apps/meteor/ee/server/models/raw/LivechatUnit.ts b/apps/meteor/ee/server/models/raw/LivechatUnit.ts index fcabf12fa4f8..c198ee04fbb0 100644 --- a/apps/meteor/ee/server/models/raw/LivechatUnit.ts +++ b/apps/meteor/ee/server/models/raw/LivechatUnit.ts @@ -11,7 +11,6 @@ const addQueryRestrictions = async (originalQuery: Filter implement // remove other departments for await (const departmentId of savedDepartments) { if (!departmentsToSave.includes(departmentId)) { - await LivechatDepartment.updateOne( - { _id: departmentId }, - { - $set: { - parentId: null, - ancestors: null, - }, - }, - ); + await LivechatDepartment.removeDepartmentFromUnit(departmentId); } } for await (const departmentId of departmentsToSave) { - await LivechatDepartment.updateOne( - { _id: departmentId }, - { - $set: { - parentId: _id, - ancestors, - }, - }, - ); + await LivechatDepartment.addDepartmentToUnit(departmentId, _id, ancestors); } await LivechatRooms.associateRoomsWithDepartmentToUnit(departmentsToSave, _id); @@ -154,6 +137,14 @@ export class LivechatUnitRaw extends BaseRaw implement return this.updateMany(query, update); } + incrementDepartmentsCount(_id: string): Promise { + return this.updateOne({ _id }, { $inc: { numDepartments: 1 } }); + } + + decrementDepartmentsCount(_id: string): Promise { + return this.updateOne({ _id }, { $inc: { numDepartments: -1 } }); + } + async removeById(_id: string): Promise { await LivechatUnitMonitors.removeByUnitId(_id); await this.removeParentAndAncestorById(_id); diff --git a/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts new file mode 100644 index 000000000000..8fbf0dcf97a2 --- /dev/null +++ b/apps/meteor/ee/tests/unit/apps/livechat-enterprise/server/hooks/manageDepartmentUnit.spec.ts @@ -0,0 +1,183 @@ +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const livechatDepartmentStub = { + findOneById: sinon.stub(), + addDepartmentToUnit: sinon.stub(), + removeDepartmentFromUnit: sinon.stub(), +}; + +const livechatUnitStub = { + findOneById: sinon.stub(), + decrementDepartmentsCount: sinon.stub(), + incrementDepartmentsCount: sinon.stub(), +}; + +const hasAnyRoleStub = sinon.stub(); +const getUnitsFromUserStub = sinon.stub(); + +const { manageDepartmentUnit } = proxyquire + .noCallThru() + .load('../../../../../../app/livechat-enterprise/server/hooks/manageDepartmentUnit.ts', { + '../methods/getUnitsFromUserRoles': { + getUnitsFromUser: getUnitsFromUserStub, + }, + '../../../../../app/authorization/server/functions/hasRole': { + hasAnyRoleAsync: hasAnyRoleStub, + }, + '@rocket.chat/models': { + LivechatDepartment: livechatDepartmentStub, + LivechatUnit: livechatUnitStub, + }, + }); + +describe('hooks/manageDepartmentUnit', () => { + beforeEach(() => { + livechatDepartmentStub.findOneById.reset(); + livechatDepartmentStub.addDepartmentToUnit.reset(); + livechatDepartmentStub.removeDepartmentFromUnit.reset(); + livechatUnitStub.findOneById.reset(); + livechatUnitStub.decrementDepartmentsCount.reset(); + livechatUnitStub.incrementDepartmentsCount.reset(); + hasAnyRoleStub.reset(); + }); + + it('should not perform any operation when an invalid department is provided', async () => { + livechatDepartmentStub.findOneById.resolves(null); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should not perform any operation if the provided department is already in unit', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it("should not perform any operation if user is a monitor and can't manage new unit", async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it("should not perform any operation if user is a monitor and can't manage current unit", async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['new-unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should not perform any operation if user is an admin/manager but an invalid unit is provided', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + livechatUnitStub.findOneById.resolves(undefined); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should remove department from its current unit if user is an admin/manager', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: undefined }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.calledOnceWith('department-id')).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); + + it('should add department to a unit if user is an admin/manager', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'unit-id', ['unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should move department to a new unit if user is an admin/manager', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'new-unit-id' }); + hasAnyRoleStub.resolves(true); + getUnitsFromUserStub.resolves(undefined); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'new-unit-id', ['new-unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('new-unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); + + it('should remove department from its current unit if user is a monitor that supervises the current unit', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: undefined }); + expect(livechatDepartmentStub.addDepartmentToUnit.notCalled).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.notCalled).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.calledOnceWith('department-id')).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); + + it('should add department to a unit if user is a monitor that supervises the new unit', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'unit-id', ['unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.notCalled).to.be.true; + }); + + it('should move department to a new unit if user is a monitor that supervises the current and new units', async () => { + livechatDepartmentStub.findOneById.resolves({ _id: 'department-id', ancestors: ['unit-id'], parentId: 'unit-id' }); + livechatUnitStub.findOneById.resolves({ _id: 'unit-id' }); + hasAnyRoleStub.resolves(false); + getUnitsFromUserStub.resolves(['unit-id', 'new-unit-id']); + + await manageDepartmentUnit({ userId: 'user-id', departmentId: 'department-id', unitId: 'new-unit-id' }); + expect(livechatDepartmentStub.addDepartmentToUnit.calledOnceWith('department-id', 'new-unit-id', ['new-unit-id'])).to.be.true; + expect(livechatUnitStub.incrementDepartmentsCount.calledOnceWith('new-unit-id')).to.be.true; + expect(livechatDepartmentStub.removeDepartmentFromUnit.notCalled).to.be.true; + expect(livechatUnitStub.decrementDepartmentsCount.calledOnceWith('unit-id')).to.be.true; + }); +}); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 7eaa9ed7595d..57b8527d5008 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -225,6 +225,7 @@ type ChainedCallbackSignatures = { 'unarchiveRoom': (room: IRoom) => void; 'roomAvatarChanged': (room: IRoom) => void; 'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise; + 'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void; }; export type Hook = @@ -247,6 +248,7 @@ export type Hook = | 'livechat.offlineMessage' | 'livechat.onCheckRoomApiParams' | 'livechat.onLoadConfigApi' + | 'livechat.manageDepartmentUnit' | 'loginPageStateChange' | 'mapLDAPUserData' | 'onCreateUser' diff --git a/apps/meteor/server/models/raw/LivechatDepartment.ts b/apps/meteor/server/models/raw/LivechatDepartment.ts index b8263af030a8..9ecee34df5e9 100644 --- a/apps/meteor/server/models/raw/LivechatDepartment.ts +++ b/apps/meteor/server/models/raw/LivechatDepartment.ts @@ -222,6 +222,14 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.updateOne({ _id }, { $set: { archived: true, enabled: false } }); } + addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise { + return this.updateOne({ _id }, { $set: { parentId: unitId, ancestors } }); + } + + removeDepartmentFromUnit(_id: string): Promise { + return this.updateOne({ _id }, { $set: { parentId: null, ancestors: null } }); + } + async createOrUpdateDepartment( _id: string | null, data: { @@ -328,6 +336,10 @@ export class LivechatDepartmentRaw extends BaseRaw implemen return this.find(query, options); } + countDepartmentsInUnit(unitId: string): Promise { + return this.countDocuments({ parentId: unitId }); + } + findActiveByUnitIds(unitIds: string[], options: FindOptions = {}): FindCursor { const query = { enabled: true, diff --git a/apps/meteor/tests/data/livechat/department.ts b/apps/meteor/tests/data/livechat/department.ts index 72ab0af9f267..d7f22fca970b 100644 --- a/apps/meteor/tests/data/livechat/department.ts +++ b/apps/meteor/tests/data/livechat/department.ts @@ -42,36 +42,44 @@ const updateDepartment = async (departmentId: string, departmentData: Partial

  • +export const createDepartmentWithMethod = ({ + initialAgents = [], + allowReceiveForwardOffline = false, + fallbackForwardDepartment, + name, + departmentUnit, + userCredentials = credentials, + departmentId = '', +}: { + initialAgents?: { agentId: string; username: string }[]; + allowReceiveForwardOffline?: boolean; + fallbackForwardDepartment?: string; + name?: string; + departmentUnit?: { _id?: string }; + userCredentials?: Credentials; + departmentId?: string; +} = {}): Promise => new Promise((resolve, reject) => { void request .post(methodCall('livechat:saveDepartment')) - .set(credentials) + .set(userCredentials) .send({ message: JSON.stringify({ method: 'livechat:saveDepartment', params: [ - '', + departmentId, { enabled: true, email: faker.internet.email(), showOnRegistration: true, showOnOfflineForm: true, - name: `new department ${Date.now()}`, + name: name || `new department ${Date.now()}`, description: 'created from api', allowReceiveForwardOffline, fallbackForwardDepartment, }, initialAgents, + departmentUnit, ], id: 'id', msg: 'method', @@ -93,7 +101,7 @@ type OnlineAgent = { export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => { const { user, credentials } = await createAnOnlineAgent(); - const department = (await createDepartmentWithMethod()) as ILivechatDepartment; + const department = await createDepartmentWithMethod(); await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); @@ -108,7 +116,7 @@ export const createDepartmentWithAnOnlineAgent = async (): Promise<{ department: export const createDepartmentWithAgent = async (agent: OnlineAgent): Promise<{ department: ILivechatDepartment; agent: OnlineAgent }> => { const { user, credentials } = agent; - const department = (await createDepartmentWithMethod()) as ILivechatDepartment; + const department = await createDepartmentWithMethod(); await addOrRemoveAgentFromDepartment(department._id, { agentId: user._id, username: user.username }, true); @@ -153,7 +161,7 @@ export const createDepartmentWithAnOfflineAgent = async ({ }> => { const { user, credentials } = await createAnOfflineAgent(); - const department = (await createDepartmentWithMethod(undefined, { + const department = (await createDepartmentWithMethod({ allowReceiveForwardOffline, fallbackForwardDepartment, })) as ILivechatDepartment; diff --git a/apps/meteor/tests/data/livechat/rooms.ts b/apps/meteor/tests/data/livechat/rooms.ts index b5d89762c614..46e5cbe489a9 100644 --- a/apps/meteor/tests/data/livechat/rooms.ts +++ b/apps/meteor/tests/data/livechat/rooms.ts @@ -98,11 +98,55 @@ export const createDepartment = ( name?: string, enabled = true, opts: Record = {}, + departmentUnit?: { _id?: string }, + userCredentials: Credentials = credentials, ): Promise => { return new Promise((resolve, reject) => { void request .post(api('livechat/department')) - .set(credentials) + .set(userCredentials) + .send({ + department: { + name: name || `Department ${Date.now()}`, + enabled, + showOnOfflineForm: true, + showOnRegistration: true, + email: 'a@b.com', + ...opts, + }, + agents, + departmentUnit, + }) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(res.body.department); + }); + }); +}; + +export const updateDepartment = ({ + departmentId, + userCredentials, + agents, + name, + enabled = true, + opts = {}, + departmentUnit, +}: { + departmentId: string; + userCredentials: Credentials; + agents?: { agentId: string }[]; + name?: string; + enabled?: boolean; + opts?: Record; + departmentUnit?: { _id?: string }; +}): Promise => { + return new Promise((resolve, reject) => { + void request + .put(api(`livechat/department/${departmentId}`)) + .set(userCredentials) .send({ department: { name: name || `Department ${Date.now()}`, @@ -113,6 +157,7 @@ export const createDepartment = ( ...opts, }, agents, + departmentUnit, }) .end((err: Error, res: DummyResponse) => { if (err) { diff --git a/apps/meteor/tests/data/livechat/units.ts b/apps/meteor/tests/data/livechat/units.ts index 03ea578e654d..8a2d0f5a833a 100644 --- a/apps/meteor/tests/data/livechat/units.ts +++ b/apps/meteor/tests/data/livechat/units.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import type { IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; -import { methodCall, credentials, request } from '../api-data'; +import { methodCall, credentials, request, api } from '../api-data'; import type { DummyResponse } from './utils'; export const createMonitor = async (username: string): Promise<{ _id: string; username: string }> => { @@ -57,3 +57,39 @@ export const createUnit = async ( }); }); }; + +export const deleteUnit = async (unit: IOmnichannelBusinessUnit): Promise => { + return new Promise((resolve, reject) => { + void request + .post(methodCall(`livechat:removeUnit`)) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:removeUnit', + params: [unit._id], + id: '101', + msg: 'method', + }), + }) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(JSON.parse(res.body.message).result); + }); + }); +}; + +export const getUnit = (unitId: string): Promise => { + return new Promise((resolve, reject) => { + void request + .get(api(`livechat/units/${unitId}`)) + .set(credentials) + .end((err: Error, res: DummyResponse) => { + if (err) { + return reject(err); + } + resolve(res.body); + }); + }); +}; diff --git a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts index 425c776fecdb..e0c079ece243 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/14-units.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/14-units.ts @@ -1,12 +1,14 @@ import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, after, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { createDepartment } from '../../../data/livechat/rooms'; -import { createMonitor, createUnit } from '../../../data/livechat/units'; +import { getCredentials, api, request, credentials, methodCall } from '../../../data/api-data'; +import { deleteDepartment, getDepartmentById, createDepartmentWithMethod } from '../../../data/livechat/department'; +import { createDepartment, updateDepartment } from '../../../data/livechat/rooms'; +import { createMonitor, createUnit, deleteUnit, getUnit } from '../../../data/livechat/units'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; -import { createUser, deleteUser } from '../../../data/users.helper'; +import { password } from '../../../data/user'; +import { createUser, deleteUser, login } from '../../../data/users.helper'; import { IS_EE } from '../../../e2e/config/constants'; (IS_EE ? describe : describe.skip)('[EE] LIVECHAT - Units', () => { @@ -14,6 +16,7 @@ import { IS_EE } from '../../../e2e/config/constants'; before(async () => { await updateSetting('Livechat_enabled', true); + await updatePermission('manage-livechat-departments', ['livechat-manager', 'livechat-monitor', 'admin']); }); describe('[GET] livechat/units', () => { @@ -409,4 +412,547 @@ import { IS_EE } from '../../../e2e/config/constants'; await deleteUser(user); }); }); + + describe('[POST] livechat/department', () => { + let monitor1: Awaited>; + let monitor1Credentials: Awaited>; + let monitor2: Awaited>; + let monitor2Credentials: Awaited>; + let unit: IOmnichannelBusinessUnit; + + before(async () => { + monitor1 = await createUser(); + monitor2 = await createUser(); + await createMonitor(monitor1.username); + monitor1Credentials = await login(monitor1.username, password); + await createMonitor(monitor2.username); + monitor2Credentials = await login(monitor2.username, password); + unit = await createUnit(monitor1._id, monitor1.username, []); + }); + + after(async () => Promise.all([deleteUser(monitor1), deleteUser(monitor2), deleteUnit(unit)])); + + it('should fail creating department when providing an invalid property in the department unit object', () => { + return request + .post(api('livechat/department')) + .set(credentials) + .send({ + department: { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: { invalidProperty: true }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'invalid-params'); + }); + }); + + it('should fail creating a department into an existing unit that a monitor does not supervise', async () => { + const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor2Credentials); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 0); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.not.have.property('parentId'); + expect(fullDepartment).to.not.have.property('ancestors'); + + await deleteDepartment(department._id); + }); + + it('should succesfully create a department into an existing unit as an admin', async () => { + const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + + await deleteDepartment(department._id); + }); + + it('should succesfully create a department into an existing unit that a monitor supervises', async () => { + const department = await createDepartment(undefined, undefined, undefined, undefined, { _id: unit._id }, monitor1Credentials); + + // Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + + await deleteDepartment(department._id); + }); + }); + + describe('[PUT] livechat/department', () => { + let monitor1: Awaited>; + let monitor1Credentials: Awaited>; + let monitor2: Awaited>; + let monitor2Credentials: Awaited>; + let unit: IOmnichannelBusinessUnit; + let department: ILivechatDepartment; + let baseDepartment: ILivechatDepartment; + + before(async () => { + monitor1 = await createUser(); + monitor2 = await createUser(); + await createMonitor(monitor1.username); + monitor1Credentials = await login(monitor1.username, password); + await createMonitor(monitor2.username); + monitor2Credentials = await login(monitor2.username, password); + department = await createDepartment(); + baseDepartment = await createDepartment(); + unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]); + }); + + after(async () => + Promise.all([ + deleteUser(monitor1), + deleteUser(monitor2), + deleteUnit(unit), + deleteDepartment(department._id), + deleteDepartment(baseDepartment._id), + ]), + ); + + it("should fail updating a department's unit when providing an invalid property in the department unit object", () => { + const updatedName = 'updated-department-name'; + return request + .put(api(`livechat/department/${department._id}`)) + .set(credentials) + .send({ + department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: { invalidProperty: true }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Match error: Unknown key in field departmentUnit.invalidProperty'); + }); + }); + + it("should fail updating a department's unit when providing an invalid _id type in the department unit object", () => { + const updatedName = 'updated-department-name'; + return request + .put(api(`livechat/department/${department._id}`)) + .set(credentials) + .send({ + department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: { _id: true }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error', 'Match error: Expected string, got boolean in field departmentUnit._id'); + }); + }); + + it('should fail removing the last department from a unit', () => { + const updatedName = 'updated-department-name'; + return request + .put(api(`livechat/department/${baseDepartment._id}`)) + .set(credentials) + .send({ + department: { name: updatedName, enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + departmentUnit: {}, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-unit-cant-be-empty'); + }); + }); + + it('should succesfully add an existing department to a unit as an admin', async () => { + const updatedName = 'updated-department-name'; + + const updatedDepartment = await updateDepartment({ + userCredentials: credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: { _id: unit._id }, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove an existing department from a unit as an admin', async () => { + const updatedName = 'updated-department-name'; + + const updatedDepartment = await updateDepartment({ + userCredentials: credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: {}, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should fail adding a department into an existing unit that a monitor does not supervise', async () => { + const updatedName = 'updated-department-name2'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor2Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: { _id: unit._id }, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should succesfully add a department into an existing unit that a monitor supervises', async () => { + const updatedName = 'updated-department-name3'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor1Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: { _id: unit._id }, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('name', updatedName); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should fail removing a department from a unit that a monitor does not supervise', async () => { + const updatedName = 'updated-department-name4'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor2Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: {}, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('name', updatedName); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove a department from a unit that a monitor supervises', async () => { + const updatedName = 'updated-department-name5'; + + const updatedDepartment = await updateDepartment({ + userCredentials: monitor1Credentials, + departmentId: department._id, + name: updatedName, + departmentUnit: {}, + }); + expect(updatedDepartment).to.have.property('name', updatedName); + expect(updatedDepartment).to.have.property('type', 'd'); + expect(updatedDepartment).to.have.property('_id', department._id); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(department._id); + expect(fullDepartment).to.have.property('name', updatedName); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + }); + + describe('[POST] livechat:saveDepartment', () => { + let monitor1: Awaited>; + let monitor1Credentials: Awaited>; + let monitor2: Awaited>; + let monitor2Credentials: Awaited>; + let unit: IOmnichannelBusinessUnit; + const departmentName = 'Test-Department-Livechat-Method'; + let testDepartmentId = ''; + let baseDepartment: ILivechatDepartment; + + before(async () => { + monitor1 = await createUser(); + monitor2 = await createUser(); + await createMonitor(monitor1.username); + monitor1Credentials = await login(monitor1.username, password); + await createMonitor(monitor2.username); + monitor2Credentials = await login(monitor2.username, password); + baseDepartment = await createDepartment(); + unit = await createUnit(monitor1._id, monitor1.username, [baseDepartment._id]); + }); + + after(async () => + Promise.all([ + deleteUser(monitor1), + deleteUser(monitor2), + deleteUnit(unit), + deleteDepartment(testDepartmentId), + deleteDepartment(baseDepartment._id), + ]), + ); + + it('should fail creating department when providing an invalid _id type in the department unit object', () => { + return request + .post(methodCall('livechat:saveDepartment')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: [ + '', + { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + [], + { _id: true }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.property('error').that.is.an('object'); + expect(data.error).to.have.property('errorType', 'Meteor.Error'); + expect(data.error).to.have.property('error', 'error-invalid-department-unit'); + }); + }); + + it('should fail removing last department from unit', () => { + return request + .post(methodCall('livechat:saveDepartment')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'livechat:saveDepartment', + params: [ + baseDepartment._id, + { name: 'Fail-Test', enabled: true, showOnOfflineForm: true, showOnRegistration: true, email: 'bla@bla' }, + [], + {}, + ], + id: 'id', + msg: 'method', + }), + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message').that.is.a('string'); + const data = JSON.parse(res.body.message); + expect(data).to.have.property('error').that.is.an('object'); + expect(data.error).to.have.property('errorType', 'Meteor.Error'); + expect(data.error).to.have.property('error', 'error-unit-cant-be-empty'); + }); + }); + + it('should fail creating a department into an existing unit that a monitor does not supervise', async () => { + const departmentName = 'Fail-Test'; + + const department = await createDepartmentWithMethod({ + userCredentials: monitor2Credentials, + name: departmentName, + departmentUnit: { _id: unit._id }, + }); + testDepartmentId = department._id; + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.not.have.property('parentId'); + expect(fullDepartment).to.not.have.property('ancestors'); + + await deleteDepartment(testDepartmentId); + }); + + it('should succesfully create a department into an existing unit as an admin', async () => { + const testDepartment = await createDepartmentWithMethod({ name: departmentName, departmentUnit: { _id: unit._id } }); + testDepartmentId = testDepartment._id; + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove an existing department from a unit as an admin', async () => { + await createDepartmentWithMethod({ name: departmentName, departmentUnit: {}, departmentId: testDepartmentId }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should succesfully add an existing department to a unit as an admin', async () => { + await createDepartmentWithMethod({ name: departmentName, departmentUnit: { _id: unit._id }, departmentId: testDepartmentId }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should succesfully remove a department from a unit that a monitor supervises', async () => { + await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: {}, + departmentId: testDepartmentId, + userCredentials: monitor1Credentials, + }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 1); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId').that.is.null; + expect(fullDepartment).to.have.property('ancestors').that.is.null; + }); + + it('should succesfully add an existing department to a unit that a monitor supervises', async () => { + await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: { _id: unit._id }, + departmentId: testDepartmentId, + userCredentials: monitor1Credentials, + }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + + it('should fail removing a department from a unit that a monitor does not supervise', async () => { + await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: {}, + departmentId: testDepartmentId, + userCredentials: monitor2Credentials, + }); + + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 2); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + + await deleteDepartment(testDepartmentId); + }); + + it('should succesfully create a department in a unit that a monitor supervises', async () => { + const testDepartment = await createDepartmentWithMethod({ + name: departmentName, + departmentUnit: { _id: unit._id }, + userCredentials: monitor1Credentials, + }); + testDepartmentId = testDepartment._id; + + // Deleting a department currently does not decrease its unit's counter. We must adjust this check when this is fixed + const updatedUnit = await getUnit(unit._id); + expect(updatedUnit).to.have.property('name', unit.name); + expect(updatedUnit).to.have.property('numMonitors', 1); + expect(updatedUnit).to.have.property('numDepartments', 3); + + const fullDepartment = await getDepartmentById(testDepartmentId); + expect(fullDepartment).to.have.property('parentId', unit._id); + expect(fullDepartment).to.have.property('ancestors').that.is.an('array').with.lengthOf(1); + expect(fullDepartment.ancestors?.[0]).to.equal(unit._id); + }); + }); }); diff --git a/packages/core-typings/src/ILivechatDepartment.ts b/packages/core-typings/src/ILivechatDepartment.ts index a73cf55cb235..0138a88226fb 100644 --- a/packages/core-typings/src/ILivechatDepartment.ts +++ b/packages/core-typings/src/ILivechatDepartment.ts @@ -16,6 +16,7 @@ export interface ILivechatDepartment { archived?: boolean; departmentsAllowedToForward?: string[]; maxNumberSimultaneousChat?: number; + parentId?: string; ancestors?: string[]; allowReceiveForwardOffline?: boolean; // extra optional fields diff --git a/packages/model-typings/src/models/ILivechatDepartmentModel.ts b/packages/model-typings/src/models/ILivechatDepartmentModel.ts index 75fe0f54b2eb..fe366256eff7 100644 --- a/packages/model-typings/src/models/ILivechatDepartmentModel.ts +++ b/packages/model-typings/src/models/ILivechatDepartmentModel.ts @@ -59,6 +59,7 @@ export interface ILivechatDepartmentModel extends IBaseModel>; findOneByIdOrName(_idOrName: string, options?: FindOptions): Promise; findByUnitIds(unitIds: string[], options?: FindOptions): FindCursor; + countDepartmentsInUnit(unitId: string): Promise; findActiveByUnitIds(unitIds: string[], options?: FindOptions): FindCursor; findNotArchived(options?: FindOptions): FindCursor; getBusinessHoursWithDepartmentStatuses(): Promise< @@ -73,4 +74,6 @@ export interface ILivechatDepartmentModel extends IBaseModel): FindCursor; archiveDepartment(_id: string): Promise; unarchiveDepartment(_id: string): Promise; + addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise; + removeDepartmentFromUnit(_id: string): Promise; } diff --git a/packages/model-typings/src/models/ILivechatUnitModel.ts b/packages/model-typings/src/models/ILivechatUnitModel.ts index 8858ee5b580e..24a482eccd0e 100644 --- a/packages/model-typings/src/models/ILivechatUnitModel.ts +++ b/packages/model-typings/src/models/ILivechatUnitModel.ts @@ -32,6 +32,8 @@ export interface ILivechatUnitModel extends IBaseModel departments: { departmentId: string }[], ): Promise>; removeParentAndAncestorById(parentId: string): Promise; + incrementDepartmentsCount(_id: string): Promise; + decrementDepartmentsCount(_id: string): Promise; removeById(_id: string): Promise; findOneByIdOrName(_idOrName: string, options: FindOptions): Promise; findByMonitorId(monitorId: string): Promise; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index a1c714c013b8..b494e5d0e5a9 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -576,7 +576,8 @@ type POSTLivechatDepartmentProps = { chatClosingTags?: string[]; fallbackForwardDepartment?: string; }; - agents: { agentId: string; count?: number; order?: number }[]; + agents?: { agentId: string; count?: number; order?: number }[]; + departmentUnit?: { _id?: string }; }; const POSTLivechatDepartmentSchema = { @@ -645,6 +646,15 @@ const POSTLivechatDepartmentSchema = { }, nullable: true, }, + departmentUnit: { + type: 'object', + properties: { + _id: { + type: 'string', + }, + }, + additionalProperties: false, + }, }, required: ['department'], additionalProperties: false, From fdde637f92928f0f59d3b089c889fdfe979100d0 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 18 Sep 2024 11:29:22 -0300 Subject: [PATCH 22/57] fix: Local avatars prioritized over external avatar provider and remove remnant references on client of `Accounts_AvatarExternalProviderUrl` (#33296) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/strong-grapes-brake.md | 9 +++++++++ apps/meteor/app/utils/client/getUserAvatarURL.ts | 5 ----- apps/meteor/app/utils/server/getUserAvatarURL.ts | 5 ----- apps/meteor/client/providers/AvatarUrlProvider.tsx | 8 ++------ apps/meteor/server/routes/avatar/user.js | 14 +++++++------- 5 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 .changeset/strong-grapes-brake.md diff --git a/.changeset/strong-grapes-brake.md b/.changeset/strong-grapes-brake.md new file mode 100644 index 000000000000..c867600a8cd2 --- /dev/null +++ b/.changeset/strong-grapes-brake.md @@ -0,0 +1,9 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed remaining direct references to external user avatar URLs + +Fixed local avatars having priority over external provider + +It mainly corrects the behavior of E2E encryption messages and desktop notifications. diff --git a/apps/meteor/app/utils/client/getUserAvatarURL.ts b/apps/meteor/app/utils/client/getUserAvatarURL.ts index 1a825a44fc27..d5fdd2b2d427 100644 --- a/apps/meteor/app/utils/client/getUserAvatarURL.ts +++ b/apps/meteor/app/utils/client/getUserAvatarURL.ts @@ -1,11 +1,6 @@ -import { settings } from '../../settings/client'; import { getAvatarURL } from './getAvatarURL'; export const getUserAvatarURL = function (username: string, cache = ''): string | undefined { - const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); - if (externalSource !== '') { - return externalSource.replace('{username}', username); - } if (username == null) { return; } diff --git a/apps/meteor/app/utils/server/getUserAvatarURL.ts b/apps/meteor/app/utils/server/getUserAvatarURL.ts index b83efea1d842..d5fdd2b2d427 100644 --- a/apps/meteor/app/utils/server/getUserAvatarURL.ts +++ b/apps/meteor/app/utils/server/getUserAvatarURL.ts @@ -1,11 +1,6 @@ -import { settings } from '../../settings/server'; import { getAvatarURL } from './getAvatarURL'; export const getUserAvatarURL = function (username: string, cache = ''): string | undefined { - const externalSource = (settings.get('Accounts_AvatarExternalProviderUrl') || '').trim().replace(/\/$/, ''); - if (externalSource !== '') { - return externalSource.replace('{username}', username); - } if (username == null) { return; } diff --git a/apps/meteor/client/providers/AvatarUrlProvider.tsx b/apps/meteor/client/providers/AvatarUrlProvider.tsx index b5a92c9117f2..606da39360d5 100644 --- a/apps/meteor/client/providers/AvatarUrlProvider.tsx +++ b/apps/meteor/client/providers/AvatarUrlProvider.tsx @@ -1,4 +1,4 @@ -import { AvatarUrlContext, useSetting } from '@rocket.chat/ui-contexts'; +import { AvatarUrlContext } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; import React, { useMemo } from 'react'; @@ -10,19 +10,15 @@ type AvatarUrlProviderProps = { }; const AvatarUrlProvider = ({ children }: AvatarUrlProviderProps) => { - const cdnAvatarUrl = String(useSetting('CDN_PREFIX') || ''); const contextValue = useMemo( () => ({ getUserPathAvatar: ((): ((uid: string, etag?: string) => string) => { - if (cdnAvatarUrl) { - return (uid: string, etag?: string): string => `${cdnAvatarUrl}/avatar/${uid}${etag ? `?etag=${etag}` : ''}`; - } return (uid: string, etag?: string): string => getURL(`/avatar/${uid}${etag ? `?etag=${etag}` : ''}`); })(), getRoomPathAvatar: ({ type, ...room }: any): string => roomCoordinator.getRoomDirectives(type || room.t).getAvatarPath({ username: room._id, ...room }) || '', }), - [cdnAvatarUrl], + [], ); return ; diff --git a/apps/meteor/server/routes/avatar/user.js b/apps/meteor/server/routes/avatar/user.js index 269c2e90019a..1c92d24fe300 100644 --- a/apps/meteor/server/routes/avatar/user.js +++ b/apps/meteor/server/routes/avatar/user.js @@ -32,6 +32,13 @@ export const userAvatar = async function (req, res) { return; } + if (settings.get('Accounts_AvatarExternalProviderUrl')) { + const response = await fetch(settings.get('Accounts_AvatarExternalProviderUrl').replace('{username}', requestUsername)); + response.headers.forEach((value, key) => res.setHeader(key, value)); + response.body.pipe(res); + return; + } + const reqModifiedHeader = req.headers['if-modified-since']; const file = await Avatars.findOneByName(requestUsername); @@ -52,13 +59,6 @@ export const userAvatar = async function (req, res) { return FileUpload.get(file, req, res); } - if (settings.get('Accounts_AvatarExternalProviderUrl')) { - const response = await fetch(settings.get('Accounts_AvatarExternalProviderUrl').replace('{username}', requestUsername)); - response.headers.forEach((value, key) => res.setHeader(key, value)); - response.body.pipe(res); - return; - } - // if still using "letters fallback" if (!wasFallbackModified(reqModifiedHeader, res)) { res.writeHead(304); From ba8bee484b77fd83359f95a3c9c52a33e46b667b Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Wed, 18 Sep 2024 12:21:29 -0300 Subject: [PATCH 23/57] fix: Mark as unread not working (#32939) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/hot-balloons-travel.md | 5 ++ .../client/actionButton.ts | 4 +- .../RoomList/SideBarItemTemplateWithData.tsx | 5 +- apps/meteor/client/sidebar/RoomMenu.tsx | 10 ++- .../RoomList/SidebarItemTemplateWithData.tsx | 3 +- apps/meteor/client/sidebarv2/RoomMenu.tsx | 10 ++- apps/meteor/tests/e2e/mark-unread.spec.ts | 71 +++++++++++++++++++ .../page-objects/fragments/home-sidenav.ts | 22 +++++- .../tests/e2e/page-objects/home-channel.ts | 4 ++ 9 files changed, 125 insertions(+), 9 deletions(-) create mode 100644 .changeset/hot-balloons-travel.md create mode 100644 apps/meteor/tests/e2e/mark-unread.spec.ts diff --git a/.changeset/hot-balloons-travel.md b/.changeset/hot-balloons-travel.md new file mode 100644 index 000000000000..d6154babc49d --- /dev/null +++ b/.changeset/hot-balloons-travel.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed issue where when you marked a room as unread and you were part of it, sometimes it would mark it as read right after diff --git a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts index fc4a9d80c43c..e1e35d216029 100644 --- a/apps/meteor/app/message-mark-as-unread/client/actionButton.ts +++ b/apps/meteor/app/message-mark-as-unread/client/actionButton.ts @@ -19,7 +19,6 @@ Meteor.startup(() => { const { message = messageArgs(this).msg } = props; try { - await sdk.call('unreadMessages', message); const subscription = ChatSubscription.findOne({ rid: message.rid, }); @@ -27,8 +26,9 @@ Meteor.startup(() => { if (subscription == null) { return; } + router.navigate('/home'); await LegacyRoomManager.close(subscription.t + subscription.name); - return router.navigate('/home'); + await sdk.call('unreadMessages', message); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx index cc7cdfbe7761..35d098151204 100644 --- a/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SideBarItemTemplateWithData.tsx @@ -167,7 +167,7 @@ function SideBarItemTemplateWithData({ const badges = ( {showBadge && isUnread && ( - + {unread + tunread?.length} )} @@ -180,6 +180,7 @@ function SideBarItemTemplateWithData({ is='a' id={id} data-qa='sidebar-item' + data-unread={highlighted} unread={highlighted} selected={selected} href={href} @@ -205,7 +206,7 @@ function SideBarItemTemplateWithData({ threadUnread={threadUnread} rid={rid} unread={!!unread} - roomOpen={false} + roomOpen={selected} type={type} cl={cl} name={title} diff --git a/apps/meteor/client/sidebar/RoomMenu.tsx b/apps/meteor/client/sidebar/RoomMenu.tsx index 06b1352d2803..05f02220104b 100644 --- a/apps/meteor/client/sidebar/RoomMenu.tsx +++ b/apps/meteor/client/sidebar/RoomMenu.tsx @@ -13,6 +13,7 @@ import { useTranslation, useEndpoint, } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; @@ -100,6 +101,8 @@ const RoomMenu = ({ const isOmnichannelRoom = type === 'l'; const prioritiesMenu = useOmnichannelPrioritiesMenu(rid); + const queryClient = useQueryClient(); + const canLeave = ((): boolean => { if (type === 'c' && !canLeaveChannel) { return false; @@ -173,17 +176,22 @@ const RoomMenu = ({ const handleToggleRead = useMutableCallback(async () => { try { + queryClient.invalidateQueries(['sidebar/search/spotlight']); + if (isUnread) { await readMessages({ rid, readThreads: true }); return; } - await unreadMessages(undefined, rid); + if (subscription == null) { return; } + LegacyRoomManager.close(subscription.t + subscription.name); router.navigate('/home'); + + await unreadMessages(undefined, rid); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx index 51b8ce495af6..e1f66ba93b4e 100644 --- a/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebarv2/RoomList/SidebarItemTemplateWithData.tsx @@ -180,6 +180,7 @@ const SidebarItemTemplateWithData = ({ is='a' id={id} data-qa='sidebar-item' + data-unread={highlighted} unread={highlighted} selected={selected} href={href} @@ -205,7 +206,7 @@ const SidebarItemTemplateWithData = ({ threadUnread={threadUnread} rid={rid} unread={!!unread} - roomOpen={false} + roomOpen={selected} type={type} cl={cl} name={title} diff --git a/apps/meteor/client/sidebarv2/RoomMenu.tsx b/apps/meteor/client/sidebarv2/RoomMenu.tsx index e88225df40ca..aac68b9ef79e 100644 --- a/apps/meteor/client/sidebarv2/RoomMenu.tsx +++ b/apps/meteor/client/sidebarv2/RoomMenu.tsx @@ -13,6 +13,7 @@ import { useTranslation, useEndpoint, } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { memo, useMemo } from 'react'; @@ -100,6 +101,8 @@ const RoomMenu = ({ const isOmnichannelRoom = type === 'l'; const prioritiesMenu = useOmnichannelPrioritiesMenu(rid); + const queryClient = useQueryClient(); + const canLeave = ((): boolean => { if (type === 'c' && !canLeaveChannel) { return false; @@ -173,17 +176,22 @@ const RoomMenu = ({ const handleToggleRead = useEffectEvent(async () => { try { + queryClient.invalidateQueries(['sidebar/search/spotlight']); + if (isUnread) { await readMessages({ rid, readThreads: true }); return; } - await unreadMessages(undefined, rid); + if (subscription == null) { return; } + LegacyRoomManager.close(subscription.t + subscription.name); router.navigate('/home'); + + await unreadMessages(undefined, rid); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } diff --git a/apps/meteor/tests/e2e/mark-unread.spec.ts b/apps/meteor/tests/e2e/mark-unread.spec.ts new file mode 100644 index 000000000000..81ae93965856 --- /dev/null +++ b/apps/meteor/tests/e2e/mark-unread.spec.ts @@ -0,0 +1,71 @@ +import { createAuxContext } from './fixtures/createAuxContext'; +import { Users } from './fixtures/userStates'; +import { HomeChannel } from './page-objects'; +import { createTargetChannel } from './utils'; +import { test, expect } from './utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe.serial('mark-unread', () => { + let poHomeChannel: HomeChannel; + let targetChannel: string; + + test.beforeEach(async ({ page, api }) => { + poHomeChannel = new HomeChannel(page); + targetChannel = await createTargetChannel(api, { members: ['user2'] }); + + await page.emulateMedia({ reducedMotion: 'reduce' }); + await page.goto('/home'); + }); + + test.afterEach(async ({ api }) => { + await api.post('/channels.delete', { roomName: targetChannel }); + }); + + test.describe('Mark Unread - Sidebar Action', () => { + test('should not mark empty room as unread', async () => { + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + + await expect(poHomeChannel.sidenav.getRoomBadge(targetChannel)).not.toBeVisible(); + }); + + test('should mark a populated room as unread', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('this is a message for reply'); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + + await expect(poHomeChannel.sidenav.getRoomBadge(targetChannel)).toBeVisible(); + }); + + test('should mark a populated room as unread - search', async () => { + await poHomeChannel.sidenav.openChat(targetChannel); + await poHomeChannel.content.sendMessage('this is a message for reply'); + await poHomeChannel.sidenav.selectMarkAsUnread(targetChannel); + await poHomeChannel.sidenav.searchRoom(targetChannel); + + await expect(poHomeChannel.sidenav.getSearchChannelBadge(targetChannel)).toBeVisible(); + }); + }); + + test.describe('Mark Unread - Message Action', () => { + let poHomeChannelUser2: HomeChannel; + + test('should mark a populated room as unread', async ({ browser }) => { + const { page: user2Page } = await createAuxContext(browser, Users.user2); + poHomeChannelUser2 = new HomeChannel(user2Page); + + await poHomeChannelUser2.sidenav.openChat(targetChannel); + await poHomeChannelUser2.content.sendMessage('this is a message for reply'); + await user2Page.close(); + + await poHomeChannel.sidenav.openChat(targetChannel); + + // wait for the sidebar item to be read + await poHomeChannel.sidenav.getSidebarItemByName(targetChannel, true).waitFor(); + await poHomeChannel.content.openLastMessageMenu(); + await poHomeChannel.markUnread.click(); + + await expect(poHomeChannel.sidenav.getRoomBadge(targetChannel)).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index d0bdd5028010..823469d02a96 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -68,8 +68,18 @@ export class HomeSidenav { return this.page.locator('role=menuitemcheckbox[name="Profile"]'); } - getSidebarItemByName(name: string): Locator { - return this.page.locator(`[data-qa="sidebar-item"][aria-label="${name}"]`); + // TODO: refactor getSidebarItemByName to not use data-qa + getSidebarItemByName(name: string, isRead?: boolean): Locator { + return this.page.locator( + ['[data-qa="sidebar-item"]', `[aria-label="${name}"]`, isRead && '[data-unread="false"]'].filter(Boolean).join(''), + ); + } + + async selectMarkAsUnread(name: string) { + const sidebarItem = this.getSidebarItemByName(name); + await sidebarItem.focus(); + await sidebarItem.locator('.rcx-sidebar-item__menu').click(); + await this.page.getByRole('option', { name: 'Mark Unread' }).click(); } async selectPriority(name: string, priority: string) { @@ -170,4 +180,12 @@ export class HomeSidenav { await this.checkboxEncryption.click(); await this.btnCreate.click(); } + + getRoomBadge(roomName: string): Locator { + return this.getSidebarItemByName(roomName).getByRole('status', { exact: true }); + } + + getSearchChannelBadge(name: string): Locator { + return this.page.locator(`[data-qa="sidebar-item"][aria-label="${name}"]`).first().getByRole('status', { exact: true }); + } } diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index a2784ea4c67b..ce51896eb449 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -59,4 +59,8 @@ export class HomeChannel { get roomHeaderToolbar(): Locator { return this.page.locator('[role=toolbar][aria-label="Primary Room actions"]'); } + + get markUnread(): Locator { + return this.page.locator('role=menuitem[name="Mark Unread"]'); + } } From 6a3c25c62c33e81fb286e364dedf084c64ad8ef8 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 18 Sep 2024 10:39:21 -0600 Subject: [PATCH 24/57] refactor: Remove old `setreaction` callbacks and use new after/before callbacks (#33309) --- .../reactions/client/methods/setReaction.ts | 4 ---- .../app/reactions/server/setReaction.ts | 2 -- .../app/slackbridge/server/RocketAdapter.js | 22 +++++++++---------- apps/meteor/lib/callbacks.ts | 2 -- 4 files changed, 10 insertions(+), 20 deletions(-) diff --git a/apps/meteor/app/reactions/client/methods/setReaction.ts b/apps/meteor/app/reactions/client/methods/setReaction.ts index ed15cda9ab8e..1744d49c0ceb 100644 --- a/apps/meteor/app/reactions/client/methods/setReaction.ts +++ b/apps/meteor/app/reactions/client/methods/setReaction.ts @@ -3,7 +3,6 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Meteor } from 'meteor/meteor'; import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator'; -import { callbacks } from '../../../../lib/callbacks'; import { emoji } from '../../../emoji/client'; import { Messages, ChatRoom, Subscriptions } from '../../../models/client'; @@ -55,10 +54,8 @@ Meteor.methods({ if (!message.reactions || typeof message.reactions !== 'object' || Object.keys(message.reactions).length === 0) { delete message.reactions; Messages.update({ _id: messageId }, { $unset: { reactions: 1 } }); - await callbacks.run('unsetReaction', messageId, reaction); } else { Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); - await callbacks.run('setReaction', messageId, reaction); } } else { if (!message.reactions) { @@ -72,7 +69,6 @@ Meteor.methods({ message.reactions[reaction].usernames.push(user.username); Messages.update({ _id: messageId }, { $set: { reactions: message.reactions } }); - await callbacks.run('setReaction', messageId, reaction); } }, }); diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index d513c8dda6a5..8f9c24633407 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -85,7 +85,6 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Rooms.setReactionsInLastMessage(room._id, message.reactions); } } - await callbacks.run('unsetReaction', message._id, reaction); await callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact, oldMessage }); isReacted = false; @@ -104,7 +103,6 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Rooms.setReactionsInLastMessage(room._id, message.reactions); void notifyOnRoomChangedById(room._id); } - await callbacks.run('setReaction', message._id, reaction); await callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); isReacted = true; diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.js b/apps/meteor/app/slackbridge/server/RocketAdapter.js index f76c33fa1f81..8ba2a76dcbc2 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.js +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.js @@ -45,16 +45,16 @@ export default class RocketAdapter { rocketLogger.debug('Register for events'); callbacks.add('afterSaveMessage', this.onMessage.bind(this), callbacks.priority.LOW, 'SlackBridge_Out'); callbacks.add('afterDeleteMessage', this.onMessageDelete.bind(this), callbacks.priority.LOW, 'SlackBridge_Delete'); - callbacks.add('setReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction'); - callbacks.add('unsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); + callbacks.add('afterSetReaction', this.onSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_SetReaction'); + callbacks.add('afterUnsetReaction', this.onUnSetReaction.bind(this), callbacks.priority.LOW, 'SlackBridge_UnSetReaction'); } unregisterForEvents() { rocketLogger.debug('Unregister for events'); callbacks.remove('afterSaveMessage', 'SlackBridge_Out'); callbacks.remove('afterDeleteMessage', 'SlackBridge_Delete'); - callbacks.remove('setReaction', 'SlackBridge_SetReaction'); - callbacks.remove('unsetReaction', 'SlackBridge_UnSetReaction'); + callbacks.remove('afterSetReaction', 'SlackBridge_SetReaction'); + callbacks.remove('afterUnsetReaction', 'SlackBridge_UnSetReaction'); } async onMessageDelete(rocketMessageDeleted) { @@ -72,7 +72,7 @@ export default class RocketAdapter { } } - async onSetReaction(rocketMsgID, reaction) { + async onSetReaction(rocketMsg, { reaction }) { try { if (!this.slackBridge.isReactionsEnabled) { return; @@ -80,12 +80,11 @@ export default class RocketAdapter { rocketLogger.debug('onRocketSetReaction'); - if (rocketMsgID && reaction) { - if (this.slackBridge.reactionsMap.delete(`set${rocketMsgID}${reaction}`)) { + if (rocketMsg._id && reaction) { + if (this.slackBridge.reactionsMap.delete(`set${rocketMsg._id}${reaction}`)) { // This was a Slack reaction, we don't need to tell Slack about it return; } - const rocketMsg = await Messages.findOneById(rocketMsgID); if (rocketMsg) { for await (const slack of this.slackAdapters) { const slackChannel = slack.getSlackChannel(rocketMsg.rid); @@ -101,7 +100,7 @@ export default class RocketAdapter { } } - async onUnSetReaction(rocketMsgID, reaction) { + async onUnSetReaction(rocketMsg, { reaction }) { try { if (!this.slackBridge.isReactionsEnabled) { return; @@ -109,13 +108,12 @@ export default class RocketAdapter { rocketLogger.debug('onRocketUnSetReaction'); - if (rocketMsgID && reaction) { - if (this.slackBridge.reactionsMap.delete(`unset${rocketMsgID}${reaction}`)) { + if (rocketMsg._id && reaction) { + if (this.slackBridge.reactionsMap.delete(`unset${rocketMsg._id}${reaction}`)) { // This was a Slack unset reaction, we don't need to tell Slack about it return; } - const rocketMsg = await Messages.findOneById(rocketMsgID); if (rocketMsg) { for await (const slack of this.slackAdapters) { const slackChannel = slack.getSlackChannel(rocketMsg.rid); diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 57b8527d5008..dcfd7a021c5e 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -256,10 +256,8 @@ export type Hook = | 'onValidateLogin' | 'openBroadcast' | 'renderNotification' - | 'setReaction' | 'streamMessage' | 'streamNewMessage' - | 'unsetReaction' | 'userAvatarSet' | 'userConfirmationEmailRequested' | 'userForgotPasswordEmailRequested' From d0595a398050b55386eab8330752a24114a23bfa Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 18 Sep 2024 12:24:47 -0600 Subject: [PATCH 25/57] fix: `LivechatSessionTaken` webhook event called without `agent` param (#33209) --- .changeset/spicy-rocks-burn.md | 5 +++++ .../app/livechat/server/lib/RoutingManager.ts | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 .changeset/spicy-rocks-burn.md diff --git a/.changeset/spicy-rocks-burn.md b/.changeset/spicy-rocks-burn.md new file mode 100644 index 000000000000..6468dbbec241 --- /dev/null +++ b/.changeset/spicy-rocks-burn.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed `LivechatSessionTaken` webhook event being called without the `agent` param, which represents the agent serving the room. diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.ts b/apps/meteor/app/livechat/server/lib/RoutingManager.ts index f4a2288305e5..28e5c72efc16 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.ts +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.ts @@ -265,11 +265,20 @@ export const RoutingManager: Routing = { logger.info(`Inquiry ${inquiry._id} taken by agent ${agent.agentId}`); + // assignAgent changes the room data to add the agent serving the conversation. afterTakeInquiry expects room object to be updated + const inq = await this.assignAgent(inquiry as InquiryWithAgentInfo, room, agent); + const roomAfterUpdate = await LivechatRooms.findOneById(rid); + + if (!roomAfterUpdate) { + // This should never happen + throw new Error('error-room-not-found'); + } + callbacks.runAsync( 'livechat.afterTakeInquiry', { - inquiry: await this.assignAgent(inquiry as InquiryWithAgentInfo, room, agent), - room, + inquiry: inq, + room: roomAfterUpdate, }, agent, ); @@ -282,7 +291,7 @@ export const RoutingManager: Routing = { queuedAt: undefined, }); - return LivechatRooms.findOneById(rid); + return roomAfterUpdate; }, async transferRoom(room, guest, transferData) { From a541f64c8ca02e4448a4bfbff2699d6e96f17b69 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Sep 2024 10:42:07 -0300 Subject: [PATCH 26/57] fix: error on sendmessage stub (#33317) --- .changeset/cyan-ladybugs-thank.md | 5 +++++ apps/meteor/app/lib/client/methods/sendMessage.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/cyan-ladybugs-thank.md diff --git a/.changeset/cyan-ladybugs-thank.md b/.changeset/cyan-ladybugs-thank.md new file mode 100644 index 000000000000..377a014fcb72 --- /dev/null +++ b/.changeset/cyan-ladybugs-thank.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed error during sendmessage client stub diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index bdaca587493a..19220f901458 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -43,7 +43,7 @@ Meteor.methods({ await onClientMessageReceived(message as IMessage).then((message) => { ChatMessage.insert(message); - return callbacks.run('afterSaveMessage', message, room); + return callbacks.run('afterSaveMessage', message, { room }); }); }, }); From 40339749b3682e0c7f804787fab6e49eb9cef080 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Sep 2024 15:34:50 -0300 Subject: [PATCH 27/57] feat: contextualbar based on chat size (#33321) --- .changeset/kind-llamas-grin.md | 5 + .../Contextualbar/ContextualbarDialog.tsx | 6 +- .../client/views/room/layout/RoomLayout.tsx | 104 +++++++++++++----- 3 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 .changeset/kind-llamas-grin.md diff --git a/.changeset/kind-llamas-grin.md b/.changeset/kind-llamas-grin.md new file mode 100644 index 000000000000..fd349e82d7f9 --- /dev/null +++ b/.changeset/kind-llamas-grin.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Changed the contextualbar behavior based on chat size instead the viewport diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx index 23def16a94a1..4e4640270087 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarDialog.tsx @@ -18,7 +18,7 @@ type ContextualbarDialogProps = AriaDialogProps & ComponentProps { const ref = useRef(null); const { dialogProps } = useDialog({ 'aria-labelledby': 'contextualbarTitle', ...props }, ref); - const sizes = useLayoutSizes(); + const { contextualBar } = useLayoutSizes(); const position = useLayoutContextualBarPosition(); const { closeTab } = useRoomToolbox(); @@ -42,12 +42,12 @@ const ContextualbarDialog = (props: ContextualbarDialogProps) => { - + - + diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx index fd080b916b70..b5e29c05799f 100644 --- a/apps/meteor/client/views/room/layout/RoomLayout.tsx +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -1,7 +1,11 @@ +/* eslint-disable no-nested-ternary */ import { Box } from '@rocket.chat/fuselage'; +import { useResizeObserver } from '@rocket.chat/fuselage-hooks'; +import breakpointsDefinitions from '@rocket.chat/fuselage-tokens/breakpoints.json'; import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; +import { LayoutContext, useLayout } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement, ReactNode } from 'react'; -import React, { Suspense } from 'react'; +import React, { Suspense, useMemo } from 'react'; import { ContextualbarDialog } from '../../../components/Contextualbar'; import HeaderSkeleton from '../Header/HeaderSkeleton'; @@ -14,36 +18,78 @@ type RoomLayoutProps = { aside?: ReactNode; } & ComponentProps; -const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): ReactElement => ( - - - - - - - - - - } +const useBreakpointsElement = () => { + const { ref, borderBoxSize } = useResizeObserver({ + debounceDelay: 30, + }); + + const breakpoints = useMemo( + () => + breakpointsDefinitions + .filter(({ minViewportWidth }) => minViewportWidth && borderBoxSize.inlineSize && borderBoxSize.inlineSize >= minViewportWidth) + .map(({ name }) => name), + [borderBoxSize], + ); + + return { + ref, + breakpoints, + }; +}; + +const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): ReactElement => { + const { ref, breakpoints } = useBreakpointsElement(); + + const contextualbarPosition = breakpoints.includes('md') ? 'relative' : 'absolute'; + const contextualbarSize = breakpoints.includes('sm') ? (breakpoints.includes('xl') ? '38%' : '380px') : '100%'; + + const layout = useLayout(); + + return ( + ({ + ...layout, + contextualBarPosition: contextualbarPosition, + size: { + ...layout.size, + contextualBar: contextualbarSize, + }, + }), + [layout, contextualbarPosition, contextualbarSize], + )} > - {header} - - - - - {body} + + + + + + + + + + } + > + {header} + + + + + {body} + + {footer && {footer}} + + {aside && ( + + {aside} + + )} - {footer && {footer}} - {aside && ( - - {aside} - - )} - - -); + + ); +}; export default RoomLayout; From 12d630799827e75e4b3964881e79fe28d0ad81d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:24:05 +0100 Subject: [PATCH 28/57] feat: `RoomSidepanel` (#33225) Co-authored-by: Guilherme Gazzo <5263975+ggazzo@users.noreply.github.com> --- .changeset/witty-lemons-type.md | 10 ++ apps/meteor/app/api/server/v1/rooms.ts | 2 +- .../FeaturePreviewSidePanelNavigation.tsx | 10 ++ .../client/hooks/useRoomInfoEndpoint.ts | 4 +- .../client/hooks/useSidePanelNavigation.ts | 14 +++ apps/meteor/client/lib/RoomManager.ts | 28 ++++- .../sidebarv2/header/CreateTeamModal.tsx | 62 ++++++++++ .../client/sidebarv2/header/SearchSection.tsx | 30 ++++- apps/meteor/client/views/room/RoomOpener.tsx | 80 +++++++------ .../views/room/Sidepanel/RoomSidepanel.tsx | 66 +++++++++++ .../Sidepanel/RoomSidepanelListWrapper.tsx | 19 ++++ .../room/Sidepanel/RoomSidepanelLoading.tsx | 20 ++++ .../SidepanelItem/RoomSidepanelItem.tsx | 29 +++++ .../room/Sidepanel/SidepanelItem/index.ts | 1 + .../room/Sidepanel/hooks/useItemData.tsx | 68 +++++++++++ .../Sidepanel/hooks/useTeamslistChildren.ts | 106 ++++++++++++++++++ .../client/views/room/Sidepanel/index.ts | 1 + .../Info/EditRoomInfo/EditRoomInfo.tsx | 68 ++++++++++- .../EditRoomInfo/useEditRoomInitialValues.ts | 18 ++- .../client/views/room/layout/RoomLayout.tsx | 4 +- .../views/room/providers/RoomProvider.tsx | 77 +++++++++++-- apps/meteor/lib/publishFields.ts | 1 + apps/meteor/server/services/team/service.ts | 6 +- packages/core-typings/src/IRoom.ts | 3 + packages/i18n/src/locales/en.i18n.json | 4 +- packages/rest-typings/src/v1/rooms.ts | 2 +- .../FeaturePreview/FeaturePreview.tsx | 12 +- .../src/hooks/useFeaturePreviewList.ts | 2 +- 28 files changed, 689 insertions(+), 58 deletions(-) create mode 100644 .changeset/witty-lemons-type.md create mode 100644 apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx create mode 100644 apps/meteor/client/hooks/useSidePanelNavigation.ts create mode 100644 apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts create mode 100644 apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx create mode 100644 apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts create mode 100644 apps/meteor/client/views/room/Sidepanel/index.ts diff --git a/.changeset/witty-lemons-type.md b/.changeset/witty-lemons-type.md new file mode 100644 index 000000000000..a007cbe6260e --- /dev/null +++ b/.changeset/witty-lemons-type.md @@ -0,0 +1,10 @@ +--- +'@rocket.chat/core-services': minor +'@rocket.chat/model-typings': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +'@rocket.chat/ui-client': minor +'@rocket.chat/meteor': minor +--- + +Implemented new feature preview for Sidepanel diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 5da59d977fb1..3dc62e462ddf 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -420,7 +420,7 @@ API.v1.addRoute( const discussionParent = room.prid && (await Rooms.findOneById>(room.prid, { - projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1 }, + projection: { name: 1, fname: 1, t: 1, prid: 1, u: 1, sidepanel: 1 }, })); const { team, parentRoom } = await Team.getRoomInfo(room); const parent = discussionParent || parentRoom; diff --git a/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx b/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx new file mode 100644 index 000000000000..f5d658ccb2f2 --- /dev/null +++ b/apps/meteor/client/components/FeaturePreviewSidePanelNavigation.tsx @@ -0,0 +1,10 @@ +import { FeaturePreview } from '@rocket.chat/ui-client'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { useSidePanelNavigationScreenSize } from '../hooks/useSidePanelNavigation'; + +export const FeaturePreviewSidePanelNavigation = ({ children }: { children: ReactElement[] }) => { + const disabled = !useSidePanelNavigationScreenSize(); + return ; +}; diff --git a/apps/meteor/client/hooks/useRoomInfoEndpoint.ts b/apps/meteor/client/hooks/useRoomInfoEndpoint.ts index 47ea84af20b6..0bac1d7eb413 100644 --- a/apps/meteor/client/hooks/useRoomInfoEndpoint.ts +++ b/apps/meteor/client/hooks/useRoomInfoEndpoint.ts @@ -1,6 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { OperationResult } from '@rocket.chat/rest-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint, useUserId } from '@rocket.chat/ui-contexts'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query'; import { minutesToMilliseconds } from 'date-fns'; @@ -8,6 +8,7 @@ import type { Meteor } from 'meteor/meteor'; export const useRoomInfoEndpoint = (rid: IRoom['_id']): UseQueryResult> => { const getRoomInfo = useEndpoint('GET', '/v1/rooms.info'); + const uid = useUserId(); return useQuery(['/v1/rooms.info', rid], () => getRoomInfo({ roomId: rid }), { cacheTime: minutesToMilliseconds(15), staleTime: minutesToMilliseconds(5), @@ -17,5 +18,6 @@ export const useRoomInfoEndpoint = (rid: IRoom['_id']): UseQueryResult { + const isSidepanelFeatureEnabled = useFeaturePreview('sidepanelNavigation'); + // ["xs", "sm", "md", "lg", "xl", xxl"] + return useSidePanelNavigationScreenSize() && isSidepanelFeatureEnabled; +}; + +export const useSidePanelNavigationScreenSize = () => { + const breakpoints = useBreakpoints(); + // ["xs", "sm", "md", "lg", "xl", xxl"] + return breakpoints.includes('lg'); +}; diff --git a/apps/meteor/client/lib/RoomManager.ts b/apps/meteor/client/lib/RoomManager.ts index 34f64e4f4c78..840493aae406 100644 --- a/apps/meteor/client/lib/RoomManager.ts +++ b/apps/meteor/client/lib/RoomManager.ts @@ -55,6 +55,8 @@ export const RoomManager = new (class RoomManager extends Emitter<{ private rooms: Map = new Map(); + private parentRid?: IRoom['_id'] | undefined; + constructor() { super(); debugRoomManager && @@ -78,6 +80,13 @@ export const RoomManager = new (class RoomManager extends Emitter<{ } get opened(): IRoom['_id'] | undefined { + return this.parentRid ?? this.rid; + } + + get openedSecondLevel(): IRoom['_id'] | undefined { + if (!this.parentRid) { + return undefined; + } return this.rid; } @@ -106,20 +115,28 @@ export const RoomManager = new (class RoomManager extends Emitter<{ this.emit('changed', this.rid); } - open(rid: IRoom['_id']): void { + private _open(rid: IRoom['_id'], parent?: IRoom['_id']): void { if (rid === this.rid) { return; } - this.back(rid); if (!this.rooms.has(rid)) { this.rooms.set(rid, new RoomStore(rid)); } this.rid = rid; + this.parentRid = parent; this.emit('opened', this.rid); this.emit('changed', this.rid); } + open(rid: IRoom['_id']): void { + this._open(rid); + } + + openSecondLevel(parentId: IRoom['_id'], rid: IRoom['_id']): void { + this._open(rid, parentId); + } + getStore(rid: IRoom['_id']): RoomStore | undefined { return this.rooms.get(rid); } @@ -130,4 +147,11 @@ const subscribeOpenedRoom = [ (): IRoom['_id'] | undefined => RoomManager.opened, ] as const; +const subscribeOpenedSecondLevelRoom = [ + (callback: () => void): (() => void) => RoomManager.on('changed', callback), + (): IRoom['_id'] | undefined => RoomManager.openedSecondLevel, +] as const; + export const useOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedRoom); + +export const useSecondLevelOpenedRoom = (): IRoom['_id'] | undefined => useSyncExternalStore(...subscribeOpenedSecondLevelRoom); diff --git a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx index a7e7b506de0f..9de721d8bbcd 100644 --- a/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx +++ b/apps/meteor/client/sidebarv2/header/CreateTeamModal.tsx @@ -1,3 +1,4 @@ +import type { SidepanelItem } from '@rocket.chat/core-typings'; import { Box, Button, @@ -16,6 +17,7 @@ import { AccordionItem, } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import { useEndpoint, usePermission, @@ -40,6 +42,8 @@ type CreateTeamModalInputs = { encrypted: boolean; broadcast: boolean; members?: string[]; + showDiscussions?: boolean; + showChannels?: boolean; }; type CreateTeamModalProps = { onClose: () => void }; @@ -50,6 +54,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { const e2eEnabledForPrivateByDefault = useSetting('E2E_Enabled_Default_PrivateRooms'); const namesValidation = useSetting('UTF8_Channel_Names_Validation'); const allowSpecialNames = useSetting('UI_Allow_room_names_with_special_chars'); + const dispatchToastMessage = useToastMessageDispatch(); const canCreateTeam = usePermission('create-team'); const canSetReadOnly = usePermissionWithScopedRoles('set-readonly', ['owner']); @@ -94,6 +99,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { encrypted: (e2eEnabledForPrivateByDefault as boolean) ?? false, broadcast: false, members: [], + showChannels: true, + showDiscussions: true, }, }); @@ -123,7 +130,10 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { topic, broadcast, encrypted, + showChannels, + showDiscussions, }: CreateTeamModalInputs): Promise => { + const sidepanelItem = [showChannels && 'channels', showDiscussions && 'discussions'].filter(Boolean) as [SidepanelItem, SidepanelItem?]; const params = { name, members, @@ -136,6 +146,7 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { encrypted, }, }, + ...((showChannels || showDiscussions) && { sidepanel: { items: sidepanelItem } }), }; try { @@ -157,6 +168,8 @@ const CreateTeamModal = ({ onClose }: CreateTeamModalProps) => { const encryptedId = useUniqueId(); const broadcastId = useUniqueId(); const addMembersId = useUniqueId(); + const showChannelsId = useUniqueId(); + const showDiscussionsId = useUniqueId(); return ( { + + {null} + + + + {t('Navigation')} + + + + {t('Channels')} + ( + + )} + /> + + {t('Show_channels_description')} + + + + + {t('Discussions')} + ( + + )} + /> + + {t('Show_discussions_description')} + + + + {t('Security_and_permissions')} diff --git a/apps/meteor/client/sidebarv2/header/SearchSection.tsx b/apps/meteor/client/sidebarv2/header/SearchSection.tsx index 660b8ee19cd5..71b26b2e4053 100644 --- a/apps/meteor/client/sidebarv2/header/SearchSection.tsx +++ b/apps/meteor/client/sidebarv2/header/SearchSection.tsx @@ -22,6 +22,32 @@ const wrapperStyle = css` background-color: ${Palette.surface['surface-sidebar']}; `; +const mobileCheck = function () { + let check = false; + (function (a: string) { + if ( + /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( + a, + ) || + /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test( + a.substr(0, 4), + ) + ) + check = true; + })(navigator.userAgent || navigator.vendor || window.opera || ''); + return check; +}; + +const shortcut = ((): string => { + if (navigator.userAgentData?.mobile || mobileCheck()) { + return ''; + } + if (window.navigator.platform.toLowerCase().includes('mac')) { + return '(\u2318+K)'; + } + return '(Ctrl+K)'; +})(); + const SearchSection = () => { const t = useTranslation(); const user = useUser(); @@ -68,11 +94,13 @@ const SearchSection = () => { }; }, [handleEscSearch, setFocus]); + const placeholder = [t('Search'), shortcut].filter(Boolean).join(' '); + return ( import('./providers/RoomProvider')); @@ -23,46 +26,59 @@ type RoomOpenerProps = { reference: string; }; +const isDirectOrOmnichannelRoom = (type: RoomType) => type === 'd' || type === 'l'; + const RoomOpener = ({ type, reference }: RoomOpenerProps): ReactElement => { const { data, error, isSuccess, isError, isLoading } = useOpenRoom({ type, reference }); const { t } = useTranslation(); return ( - }> - {isLoading && } - {isSuccess && ( - - - + + {!isDirectOrOmnichannelRoom(type) && ( + + {null} + + + + )} - {isError && - (() => { - if (error instanceof OldUrlRoomError) { - return ; - } - if (error instanceof RoomNotFoundError) { - return ; - } + }> + {isLoading && } + {isSuccess && ( + + + + )} + {isError && + (() => { + if (error instanceof OldUrlRoomError) { + return ; + } + + if (error instanceof RoomNotFoundError) { + return ; + } - if (error instanceof NotAuthorizedError) { - return ; - } + if (error instanceof NotAuthorizedError) { + return ; + } - return ( - } - body={ - - - {t('core.Error')} - {getErrorMessage(error)} - - } - /> - ); - })()} - + return ( + } + body={ + + + {t('core.Error')} + {getErrorMessage(error)} + + } + /> + ); + })()} + + ); }; diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx new file mode 100644 index 000000000000..27c45e2774e8 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx @@ -0,0 +1,66 @@ +/* eslint-disable react/no-multi-comp */ +import { Box, Sidepanel, SidepanelListItem } from '@rocket.chat/fuselage'; +import { useUserPreference } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; +import { Virtuoso } from 'react-virtuoso'; + +import { VirtuosoScrollbars } from '../../../components/CustomScrollbars'; +import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; +import { useOpenedRoom, useSecondLevelOpenedRoom } from '../../../lib/RoomManager'; +import RoomSidepanelListWrapper from './RoomSidepanelListWrapper'; +import RoomSidepanelLoading from './RoomSidepanelLoading'; +import RoomSidepanelItem from './SidepanelItem'; +import { useTeamsListChildrenUpdate } from './hooks/useTeamslistChildren'; + +const RoomSidepanel = () => { + const parentRid = useOpenedRoom(); + const secondLevelOpenedRoom = useSecondLevelOpenedRoom() ?? parentRid; + + if (!parentRid || !secondLevelOpenedRoom) { + return null; + } + + return ; +}; + +const RoomSidepanelWithData = ({ parentRid, openedRoom }: { parentRid: string; openedRoom: string }) => { + const sidebarViewMode = useUserPreference<'extended' | 'medium' | 'condensed'>('sidebarViewMode'); + + const roomInfo = useRoomInfoEndpoint(parentRid); + const sidepanelItems = roomInfo.data?.room?.sidepanel?.items || roomInfo.data?.parent?.sidepanel?.items; + + const result = useTeamsListChildrenUpdate( + parentRid, + !roomInfo.data ? null : roomInfo.data.room?.teamId, + // eslint-disable-next-line no-nested-ternary + !sidepanelItems ? null : sidepanelItems?.length === 1 ? sidepanelItems[0] : undefined, + ); + if (roomInfo.isSuccess && !roomInfo.data.room?.sidepanel && !roomInfo.data.parent?.sidepanel) { + return null; + } + + if (roomInfo.isLoading || (roomInfo.isSuccess && result.isLoading)) { + return ; + } + + if (!result.isSuccess || !roomInfo.isSuccess) { + return null; + } + + return ( + + + ( + + )} + /> + + + ); +}; + +export default memo(RoomSidepanel); diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx new file mode 100644 index 000000000000..dd9e6e6ec221 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelListWrapper.tsx @@ -0,0 +1,19 @@ +import { SidepanelList } from '@rocket.chat/fuselage'; +import { useMergedRefs } from '@rocket.chat/fuselage-hooks'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { ForwardedRef, HTMLAttributes } from 'react'; +import React, { forwardRef } from 'react'; + +import { useSidebarListNavigation } from '../../../sidebar/RoomList/useSidebarListNavigation'; + +type RoomListWrapperProps = HTMLAttributes; + +const RoomSidepanelListWrapper = forwardRef(function RoomListWrapper(props: RoomListWrapperProps, ref: ForwardedRef) { + const t = useTranslation(); + const { sidebarListRef } = useSidebarListNavigation(); + const mergedRefs = useMergedRefs(ref, sidebarListRef); + + return ; +}); + +export default RoomSidepanelListWrapper; diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx new file mode 100644 index 000000000000..00609ae6c496 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanelLoading.tsx @@ -0,0 +1,20 @@ +import { SidebarV2Item as SidebarItem, Sidepanel, SidepanelList, Skeleton } from '@rocket.chat/fuselage'; +import React from 'react'; + +const RoomSidepanelLoading = () => ( + + + + + + + + + + + + + +); + +export default RoomSidepanelLoading; diff --git a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx new file mode 100644 index 000000000000..dceb69e1aba3 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx @@ -0,0 +1,29 @@ +import type { IRoom, ISubscription, Serialized } from '@rocket.chat/core-typings'; +import { useUserSubscription } from '@rocket.chat/ui-contexts'; +import React, { memo } from 'react'; + +import { goToRoomById } from '../../../../lib/utils/goToRoomById'; +import { useTemplateByViewMode } from '../../../../sidebarv2/hooks/useTemplateByViewMode'; +import { useItemData } from '../hooks/useItemData'; + +export type RoomSidepanelItemProps = { + openedRoom?: string; + room: Serialized; + parentRid: string; + viewMode?: 'extended' | 'medium' | 'condensed'; +}; + +const RoomSidepanelItem = ({ room, openedRoom, viewMode }: RoomSidepanelItemProps) => { + const SidepanelItem = useTemplateByViewMode(); + const subscription = useUserSubscription(room._id); + + const itemData = useItemData({ ...room, ...subscription } as ISubscription & IRoom, { viewMode, openedRoom }); // as any because of divergent and overlaping timestamp types in subs and room (type Date vs type string) + + if (!subscription) { + return ; + } + + return ; +}; + +export default memo(RoomSidepanelItem); diff --git a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts new file mode 100644 index 000000000000..5cfc0da3055b --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/index.ts @@ -0,0 +1 @@ +export { default } from './RoomSidepanelItem'; diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx b/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx new file mode 100644 index 000000000000..a6de01e22084 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useItemData.tsx @@ -0,0 +1,68 @@ +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { SidebarV2ItemBadge as SidebarItemBadge, SidebarV2ItemIcon as SidebarItemIcon } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useMemo } from 'react'; + +import { RoomIcon } from '../../../../components/RoomIcon'; +import { roomCoordinator } from '../../../../lib/rooms/roomCoordinator'; +import { getBadgeTitle, getMessage } from '../../../../sidebarv2/RoomList/SidebarItemTemplateWithData'; +import { useAvatarTemplate } from '../../../../sidebarv2/hooks/useAvatarTemplate'; + +export const useItemData = ( + room: ISubscription & IRoom, + { openedRoom, viewMode }: { openedRoom: string | undefined; viewMode?: 'extended' | 'medium' | 'condensed' }, +) => { + const t = useTranslation(); + const AvatarTemplate = useAvatarTemplate(); + + const highlighted = Boolean(!room.hideUnreadStatus && (room.alert || room.unread)); + + const icon = useMemo( + () => } />, + [highlighted, room], + ); + const time = 'lastMessage' in room ? room.lastMessage?.ts : undefined; + const message = viewMode === 'extended' && getMessage(room, room.lastMessage, t); + + const threadUnread = Number(room.tunread?.length) > 0; + const isUnread = room.unread > 0 || threadUnread; + const showBadge = + !room.hideUnreadStatus || (!room.hideMentionStatus && (Boolean(room.userMentions) || Number(room.tunreadUser?.length) > 0)); + const badgeTitle = getBadgeTitle(room.userMentions, Number(room.tunread?.length), room.groupMentions, room.unread, t); + const variant = + ((room.userMentions || room.tunreadUser?.length) && 'danger') || + (threadUnread && 'primary') || + (room.groupMentions && 'warning') || + 'secondary'; + + const badges = useMemo( + () => ( + <> + {showBadge && isUnread && ( + + {room.unread + (room.tunread?.length || 0)} + + )} + + ), + [badgeTitle, isUnread, room.tunread?.length, room.unread, showBadge, variant], + ); + + const itemData = useMemo( + () => ({ + unread: highlighted, + selected: room.rid === openedRoom, + t, + href: roomCoordinator.getRouteLink(room.t, room) || '', + title: roomCoordinator.getRoomName(room.t, room) || '', + icon, + time, + badges, + avatar: AvatarTemplate && , + subtitle: message, + }), + [AvatarTemplate, badges, highlighted, icon, message, openedRoom, room, t, time], + ); + + return itemData; +}; diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts new file mode 100644 index 000000000000..5791a6e5d547 --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -0,0 +1,106 @@ +import type { IRoom } from '@rocket.chat/core-typings'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useEffect, useMemo } from 'react'; + +import { ChatRoom } from '../../../../../app/models/client'; + +const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { + if (!a.lm) { + return 1; + } + if (!b.lm) { + return -1; + } + return new Date(b.lm).toUTCString().localeCompare(new Date(a.lm).toUTCString()); +}; + +export const useTeamsListChildrenUpdate = ( + parentRid: string, + teamId?: string | null, + sidepanelItems?: 'channels' | 'discussions' | null, +) => { + const queryClient = useQueryClient(); + + const query = useMemo(() => { + const query: Parameters[0] = { + $or: [ + { + _id: parentRid, + }, + { + prid: parentRid, + }, + ], + }; + + if (teamId && query.$or) { + query.$or.push({ + teamId, + }); + } + return query; + }, [parentRid, teamId]); + + const teamList = useEndpoint('GET', '/v1/teams.listChildren'); + + const listRoomsAndDiscussions = useEndpoint('GET', '/v1/teams.listChildren'); + const result = useQuery({ + queryKey: ['sidepanel', 'list', parentRid, sidepanelItems], + queryFn: () => + listRoomsAndDiscussions({ + roomId: parentRid, + sort: JSON.stringify({ lm: -1 }), + type: sidepanelItems || undefined, + }), + enabled: sidepanelItems !== null && teamId !== null, + refetchInterval: 5 * 60 * 1000, + keepPreviousData: true, + }); + + const { mutate: update } = useMutation({ + mutationFn: async (params?: { action: 'add' | 'remove' | 'update'; data: IRoom }) => { + queryClient.setQueryData(['sidepanel', 'list', parentRid, sidepanelItems], (data: Awaited> | void) => { + if (!data) { + return; + } + + if (params?.action === 'add') { + data.data = [JSON.parse(JSON.stringify(params.data)), ...data.data].sort(sortRoomByLastMessage); + } + + if (params?.action === 'remove') { + data.data = data.data.filter((item) => item._id !== params.data?._id); + } + + if (params?.action === 'update') { + data.data = data.data + .map((item) => (item._id === params.data?._id ? JSON.parse(JSON.stringify(params.data)) : item)) + .sort(sortRoomByLastMessage); + } + + return { ...data }; + }); + }, + }); + + useEffect(() => { + const liveQueryHandle = ChatRoom.find(query).observe({ + added: (item) => { + queueMicrotask(() => update({ action: 'add', data: item })); + }, + changed: (item) => { + queueMicrotask(() => update({ action: 'update', data: item })); + }, + removed: (item) => { + queueMicrotask(() => update({ action: 'remove', data: item })); + }, + }); + + return () => { + liveQueryHandle.stop(); + }; + }, [update, query]); + + return result; +}; diff --git a/apps/meteor/client/views/room/Sidepanel/index.ts b/apps/meteor/client/views/room/Sidepanel/index.ts new file mode 100644 index 000000000000..c236142b8b0f --- /dev/null +++ b/apps/meteor/client/views/room/Sidepanel/index.ts @@ -0,0 +1 @@ +export { default } from './RoomSidepanel'; diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx index b2aac49927a1..1233768a671c 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/EditRoomInfo.tsx @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { IRoomWithRetentionPolicy } from '@rocket.chat/core-typings'; +import type { IRoomWithRetentionPolicy, SidepanelItem } from '@rocket.chat/core-typings'; import { isRoomFederated } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; import { @@ -21,10 +21,13 @@ import { Box, TextAreaInput, AccordionItem, + Divider, } from '@rocket.chat/fuselage'; import { useEffectEvent, useUniqueId } from '@rocket.chat/fuselage-hooks'; +import { FeaturePreview, FeaturePreviewOff, FeaturePreviewOn } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetting, useTranslation, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useQueryClient } from '@tanstack/react-query'; import type { ChangeEvent } from 'react'; import React, { useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; @@ -72,11 +75,12 @@ const getRetentionSetting = (roomType: IRoomWithRetentionPolicy['t']): string => }; const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => { + const query = useQueryClient(); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const isFederated = useMemo(() => isRoomFederated(room), [room]); // eslint-disable-next-line no-nested-ternary - const roomType = 'prid' in room ? 'discussion' : room.teamId ? 'team' : 'channel'; + const roomType = 'prid' in room ? 'discussion' : room.teamMain ? 'team' : 'channel'; const retentionPolicy = useRetentionPolicy(room); const retentionMaxAgeDefault = msToTimeUnit(TIMEUNIT.days, Number(useSetting(getRetentionSetting(room.t)))) ?? 30; @@ -118,6 +122,8 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => retentionOverrideGlobal, roomType: roomTypeP, reactWhenReadOnly, + showChannels, + showDiscussions, } = watch(); const { @@ -158,13 +164,23 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => retentionIgnoreThreads, ...formData }) => { - const data = getDirtyFields(formData, dirtyFields); + const data = getDirtyFields>(formData, dirtyFields); delete data.archived; + delete data.showChannels; + delete data.showDiscussions; + + const sidepanelItems = [showChannels && 'channels', showDiscussions && 'discussions'].filter(Boolean) as [ + SidepanelItem, + SidepanelItem?, + ]; + + const sidepanel = sidepanelItems.length > 0 ? { items: sidepanelItems } : null; try { await saveAction({ rid: room._id, ...data, + ...(roomType === 'team' ? { sidepanel } : null), ...((data.joinCode || 'joinCodeRequired' in data) && { joinCode: joinCodeRequired ? data.joinCode : '' }), ...((data.systemMessages || !hideSysMes) && { systemMessages: hideSysMes && data.systemMessages, @@ -180,6 +196,7 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => }), }); + await query.invalidateQueries(['/v1/rooms.info', room._id]); dispatchToastMessage({ type: 'success', message: t('Room_updated_successfully') }); onClickClose(); } catch (error) { @@ -224,6 +241,8 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => const retentionExcludePinnedField = useUniqueId(); const retentionFilesOnlyField = useUniqueId(); const retentionIgnoreThreads = useUniqueId(); + const showDiscussionsField = useUniqueId(); + const showChannelsField = useUniqueId(); const showAdvancedSettings = canViewEncrypted || canViewReadOnly || readOnly || canViewArchived || canViewJoinCode || canViewHideSysMes; const showRetentionPolicy = canEditRoomRetentionPolicy && retentionPolicy?.enabled; @@ -355,6 +374,49 @@ const EditRoomInfo = ({ room, onClickClose, onClickBack }: EditRoomInfoProps) => {showAdvancedSettings && ( + {roomType === 'team' && ( + + {null} + + + + {t('Navigation')} + + + + {t('Channels')} + ( + + )} + /> + + + {t('Show_channels_description')} + + + + + {t('Discussions')} + ( + + )} + /> + + + {t('Show_discussions_description')} + + + + + + + )} {t('Security_and_permissions')} diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts index 1a002727358f..b71f8e1b5bc5 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomInitialValues.ts @@ -10,7 +10,20 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { const retentionPolicy = useRetentionPolicy(room); const canEditRoomRetentionPolicy = usePermission('edit-room-retention-policy', room._id); - const { t, ro, archived, topic, description, announcement, joinCodeRequired, sysMes, encrypted, retention, reactWhenReadOnly } = room; + const { + t, + ro, + archived, + topic, + description, + announcement, + joinCodeRequired, + sysMes, + encrypted, + retention, + reactWhenReadOnly, + sidepanel, + } = room; return useMemo( () => ({ @@ -37,6 +50,8 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { retentionFilesOnly: retention?.filesOnly ?? retentionPolicy.filesOnly, retentionIgnoreThreads: retention?.ignoreThreads ?? retentionPolicy.ignoreThreads, }), + showDiscussions: sidepanel?.items.includes('discussions'), + showChannels: sidepanel?.items.includes('channels'), }), [ announcement, @@ -53,6 +68,7 @@ export const useEditRoomInitialValues = (room: IRoomWithRetentionPolicy) => { encrypted, reactWhenReadOnly, canEditRoomRetentionPolicy, + sidepanel, ], ); }; diff --git a/apps/meteor/client/views/room/layout/RoomLayout.tsx b/apps/meteor/client/views/room/layout/RoomLayout.tsx index b5e29c05799f..3c0c4c313c99 100644 --- a/apps/meteor/client/views/room/layout/RoomLayout.tsx +++ b/apps/meteor/client/views/room/layout/RoomLayout.tsx @@ -59,7 +59,7 @@ const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): [layout, contextualbarPosition, contextualbarSize], )} > - + @@ -82,7 +82,7 @@ const RoomLayout = ({ header, body, footer, aside, ...props }: RoomLayoutProps): {footer && {footer}} {aside && ( - + {aside} )} diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 65c83100b1c0..d67c8566e3c6 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -8,12 +8,15 @@ import { RoomHistoryManager } from '../../../../app/ui-utils/client'; import { UserAction } from '../../../../app/ui/client/lib/UserAction'; import { useReactiveQuery } from '../../../hooks/useReactiveQuery'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; +import { useRoomInfoEndpoint } from '../../../hooks/useRoomInfoEndpoint'; +import { useSidePanelNavigation } from '../../../hooks/useSidePanelNavigation'; import { RoomManager } from '../../../lib/RoomManager'; import { roomCoordinator } from '../../../lib/rooms/roomCoordinator'; import ImageGalleryProvider from '../../../providers/ImageGalleryProvider'; import RoomNotFound from '../RoomNotFound'; import RoomSkeleton from '../RoomSkeleton'; import { useRoomRolesManagement } from '../body/hooks/useRoomRolesManagement'; +import type { IRoomWithFederationOriginalName } from '../contexts/RoomContext'; import { RoomContext } from '../contexts/RoomContext'; import ComposerPopupProvider from './ComposerPopupProvider'; import RoomToolboxProvider from './RoomToolboxProvider'; @@ -30,15 +33,17 @@ type RoomProviderProps = { const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { useRoomRolesManagement(rid); - const { data: room, isSuccess } = useRoomQuery(rid); + const resultFromServer = useRoomInfoEndpoint(rid); + + const resultFromLocal = useRoomQuery(rid); // TODO: the following effect is a workaround while we don't have a general and definitive solution for it const router = useRouter(); useEffect(() => { - if (isSuccess && !room) { + if (resultFromLocal.isSuccess && !resultFromLocal.data) { router.navigate('/home'); } - }, [isSuccess, room, router]); + }, [resultFromLocal.data, resultFromLocal.isSuccess, resultFromServer, router]); const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], () => ChatSubscription.findOne({ rid }) ?? null); @@ -46,7 +51,8 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { useUsersNameChanged(); - const pseudoRoom = useMemo(() => { + const pseudoRoom: IRoomWithFederationOriginalName | null = useMemo(() => { + const room = resultFromLocal.data; if (!room) { return null; } @@ -57,7 +63,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { name: roomCoordinator.getRoomName(room.t, room), federationOriginalName: room.name, }; - }, [room, subscriptionQuery.data]); + }, [resultFromLocal.data, subscriptionQuery.data]); const { hasMorePreviousMessages, hasMoreNextMessages, isLoadingMoreMessages } = useReactiveValue( useCallback(() => { @@ -86,12 +92,69 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { }; }, [hasMoreNextMessages, hasMorePreviousMessages, isLoadingMoreMessages, pseudoRoom, rid, subscriptionQuery.data]); + const isSidepanelFeatureEnabled = useSidePanelNavigation(); + useEffect(() => { + if (isSidepanelFeatureEnabled) { + if (resultFromServer.isSuccess) { + if (resultFromServer.data.room?.teamMain) { + if ( + resultFromServer.data.room.sidepanel?.items.includes('channels') || + resultFromServer.data.room?.sidepanel?.items.includes('discussions') + ) { + RoomManager.openSecondLevel(rid, rid); + } else { + RoomManager.open(rid); + } + return (): void => { + RoomManager.back(rid); + }; + } + + switch (true) { + case resultFromServer.data.room?.prid && + resultFromServer.data.parent && + resultFromServer.data.parent.sidepanel?.items.includes('discussions'): + RoomManager.openSecondLevel(resultFromServer.data.parent._id, rid); + break; + case resultFromServer.data.team?.roomId && + !resultFromServer.data.room?.teamMain && + resultFromServer.data.parent?.sidepanel?.items.includes('channels'): + RoomManager.openSecondLevel(resultFromServer.data.team.roomId, rid); + break; + + default: + if ( + resultFromServer.data.parent?.sidepanel?.items.includes('channels') || + resultFromServer.data.parent?.sidepanel?.items.includes('discussions') + ) { + RoomManager.openSecondLevel(rid, rid); + } else { + RoomManager.open(rid); + } + break; + } + } + return (): void => { + RoomManager.back(rid); + }; + } + RoomManager.open(rid); return (): void => { RoomManager.back(rid); }; - }, [rid]); + }, [ + isSidepanelFeatureEnabled, + rid, + resultFromServer.data?.room?.prid, + resultFromServer.data?.room?.teamId, + resultFromServer.data?.room?.teamMain, + resultFromServer.isSuccess, + resultFromServer.data?.parent, + resultFromServer.data?.team?.roomId, + resultFromServer.data, + ]); const subscribed = !!subscriptionQuery.data; @@ -104,7 +167,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { }, [rid, subscribed]); if (!pseudoRoom) { - return isSuccess && !room ? : ; + return resultFromLocal.isSuccess && !resultFromLocal.data ? : ; } return ( diff --git a/apps/meteor/lib/publishFields.ts b/apps/meteor/lib/publishFields.ts index c1483ea86cd1..48d0f9c87d5c 100644 --- a/apps/meteor/lib/publishFields.ts +++ b/apps/meteor/lib/publishFields.ts @@ -74,6 +74,7 @@ export const roomFields = { avatarETag: 1, usersCount: 1, msgs: 1, + sidepanel: 1, // @TODO create an API to register this fields based on room type tags: 1, diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index f5218c88402a..4be96bff9866 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1055,8 +1055,10 @@ export class TeamService extends ServiceClassInternal implements ITeamService { return rooms; } - private getParentRoom(team: AtLeast): Promise | null> { - return Rooms.findOneById>(team.roomId, { projection: { name: 1, fname: 1, t: 1 } }); + private getParentRoom(team: AtLeast): Promise | null> { + return Rooms.findOneById>(team.roomId, { + projection: { name: 1, fname: 1, t: 1, sidepanel: 1 }, + }); } async getRoomInfo( diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 0ce1b1041de6..95f2836da1e7 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -99,6 +99,9 @@ export const isSidepanelItem = (item: any): item is SidepanelItem => { }; export const isValidSidepanel = (sidepanel: IRoom['sidepanel']) => { + if (sidepanel === null) { + return true; + } if (!sidepanel?.items) { return false; } diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 796e9d0519ff..728ca256952d 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6541,8 +6541,8 @@ "Incoming_Calls": "Incoming calls", "Advanced_settings": "Advanced settings", "Security_and_permissions": "Security and permissions", - "Sidepanel_navigation": "Sidepanel navigation for teams", - "Sidepanel_navigation_description": "Option to open a sidepanel with channels and/or discussions associated with the team. This allows team owners to customize communication methods to best meet their team’s needs. This feature is only available when Enhanced navigation experience is enabled.", + "Sidepanel_navigation": "Secondary navigation for teams", + "Sidepanel_navigation_description": "Display channels and/or discussions associated with teams by default. This allows team owners to customize communication methods to best meet their team’s needs. This is currently in feature preview and will be a premium capability once fully released.", "Show_channels_description": "Show team channels in second sidebar", "Show_discussions_description": "Show team discussions in second sidebar" } diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index c837ba7186bd..16debe87e44c 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -626,7 +626,7 @@ export type RoomsEndpoints = { '/v1/rooms.info': { GET: (params: RoomsInfoProps) => { room: IRoom | undefined; - parent?: Pick; + parent?: Pick; team?: Pick; }; }; diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx index 09ec0700cb79..c297cc839abc 100644 --- a/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreview.tsx @@ -5,8 +5,16 @@ import { Children, Suspense, cloneElement } from 'react'; import { useFeaturePreview } from '../../hooks/useFeaturePreview'; import { FeaturesAvailable } from '../../hooks/useFeaturePreviewList'; -export const FeaturePreview = ({ feature, children }: { feature: FeaturesAvailable; children: ReactElement[] }) => { - const featureToggleEnabled = useFeaturePreview(feature); +export const FeaturePreview = ({ + feature, + disabled = false, + children, +}: { + disabled?: boolean; + feature: FeaturesAvailable; + children: ReactElement[]; +}) => { + const featureToggleEnabled = useFeaturePreview(feature) && !disabled; const toggledChildren = Children.map(children, (child) => cloneElement(child, { diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index 172045197f8c..ff103a8d84ef 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -72,7 +72,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ description: 'Sidepanel_navigation_description', group: 'Navigation', value: false, - enabled: false, + enabled: true, enableQuery: { name: 'newNavigation', value: true, From 015c862c999517ad74710b951a7c47b2c00ed091 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 19 Sep 2024 20:02:19 -0300 Subject: [PATCH 29/57] ci: auto candidate releases (#33325) Co-authored-by: Diego Sampaio --- .github/workflows/release-candidate.yml | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/release-candidate.yml diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml new file mode 100644 index 000000000000..4a1e67fca33a --- /dev/null +++ b/.github/workflows/release-candidate.yml @@ -0,0 +1,35 @@ +name: Release candidate cut +on: + schedule: + - cron: '28 0 20 * *' # run at minute 28 to avoid the chance of delay due to high load on GH +jobs: + new-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + token: ${{ secrets.CI_PAT }} + + - name: Setup NodeJS + uses: ./.github/actions/setup-node + with: + node-version: 14.21.3 + cache-modules: true + install: true + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - uses: rharkor/caching-for-turbo@v1.5 + + - name: Build packages + run: yarn build + + - name: 'Start release candidate' + uses: ./packages/release-action + with: + action: next + base-ref: ${{ github.ref_name }} + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GITHUB_TOKEN: ${{ secrets.CI_PAT }} From 7faba775f0967ce745bfbe48c51561e8c1c33c48 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 19 Sep 2024 17:57:54 -0600 Subject: [PATCH 30/57] refactor: Reactions set/unset (#32994) --- apps/meteor/app/api/server/v1/chat.ts | 2 +- .../app/reactions/server/setReaction.ts | 109 +++-- apps/meteor/server/models/raw/EmojiCustom.ts | 8 + .../app/reactions/server/setReaction.spec.ts | 428 ++++++++++++++++++ .../src/models/IEmojiCustomModel.ts | 1 + 5 files changed, 499 insertions(+), 49 deletions(-) create mode 100644 apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index 3ccc9caeafa0..d04d1a2418b5 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -376,7 +376,7 @@ API.v1.addRoute( throw new Meteor.Error('error-emoji-param-not-provided', 'The required "emoji" param is missing.'); } - await executeSetReaction(this.userId, emoji, msg._id, this.bodyParams.shouldReact); + await executeSetReaction(this.userId, emoji, msg, this.bodyParams.shouldReact); return API.v1.success(); }, diff --git a/apps/meteor/app/reactions/server/setReaction.ts b/apps/meteor/app/reactions/server/setReaction.ts index 8f9c24633407..be6e5aed4a54 100644 --- a/apps/meteor/app/reactions/server/setReaction.ts +++ b/apps/meteor/app/reactions/server/setReaction.ts @@ -4,7 +4,6 @@ import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Messages, EmojiCustom, Rooms, Users } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { i18n } from '../../../server/lib/i18n'; @@ -12,26 +11,39 @@ import { canAccessRoomAsync } from '../../authorization/server'; import { hasPermissionAsync } from '../../authorization/server/functions/hasPermission'; import { emoji } from '../../emoji/server'; import { isTheLastMessage } from '../../lib/server/functions/isTheLastMessage'; -import { notifyOnRoomChangedById, notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; +import { notifyOnMessageChange } from '../../lib/server/lib/notifyListener'; -const removeUserReaction = (message: IMessage, reaction: string, username: string) => { +export const removeUserReaction = (message: IMessage, reaction: string, username: string) => { if (!message.reactions) { return message; } - message.reactions[reaction].usernames.splice(message.reactions[reaction].usernames.indexOf(username), 1); - if (message.reactions[reaction].usernames.length === 0) { + const idx = message.reactions[reaction].usernames.indexOf(username); + + // user not found in reaction array + if (idx === -1) { + return message; + } + + message.reactions[reaction].usernames.splice(idx, 1); + if (!message.reactions[reaction].usernames.length) { delete message.reactions[reaction]; } return message; }; -async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction: string, shouldReact?: boolean) { - reaction = `:${reaction.replace(/:/g, '')}:`; +export async function setReaction( + room: Pick, + user: IUser, + message: IMessage, + reaction: string, + userAlreadyReacted?: boolean, +) { + await Message.beforeReacted(message, room); - if (!emoji.list[reaction] && (await EmojiCustom.findByNameOrAlias(reaction, {}).count()) === 0) { - throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { - method: 'setReaction', + if (Array.isArray(room.muted) && room.muted.includes(user.username as string)) { + throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { + rid: room._id, }); } @@ -42,50 +54,23 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction } } - if (Array.isArray(room.muted) && room.muted.indexOf(user.username as string) !== -1) { - throw new Meteor.Error('error-not-allowed', i18n.t('You_have_been_muted', { lng: user.language }), { - rid: room._id, - }); - } - - // if (!('reactions' in message)) { - // return; - // } - - await Message.beforeReacted(message, room); - - const userAlreadyReacted = - message.reactions && - Boolean(message.reactions[reaction]) && - message.reactions[reaction].usernames.indexOf(user.username as string) !== -1; - // When shouldReact was not informed, toggle the reaction. - if (shouldReact === undefined) { - shouldReact = !userAlreadyReacted; - } - - if (userAlreadyReacted === shouldReact) { - return; - } - let isReacted; - if (userAlreadyReacted) { const oldMessage = JSON.parse(JSON.stringify(message)); removeUserReaction(message, reaction, user.username as string); - if (_.isEmpty(message.reactions)) { + if (Object.keys(message.reactions || {}).length === 0) { delete message.reactions; + await Messages.unsetReactions(message._id); if (isTheLastMessage(room, message)) { await Rooms.unsetReactionsInLastMessage(room._id); - void notifyOnRoomChangedById(room._id); } - await Messages.unsetReactions(message._id); } else { await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); } } - await callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact, oldMessage }); + void callbacks.run('afterUnsetReaction', message, { user, reaction, shouldReact: false, oldMessage }); isReacted = false; } else { @@ -101,33 +86,61 @@ async function setReaction(room: IRoom, user: IUser, message: IMessage, reaction await Messages.setReactions(message._id, message.reactions); if (isTheLastMessage(room, message)) { await Rooms.setReactionsInLastMessage(room._id, message.reactions); - void notifyOnRoomChangedById(room._id); } - await callbacks.run('afterSetReaction', message, { user, reaction, shouldReact }); + + void callbacks.run('afterSetReaction', message, { user, reaction, shouldReact: true }); isReacted = true; } - await Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); + void Apps.self?.triggerEvent(AppEvents.IPostMessageReacted, message, user, reaction, isReacted); void notifyOnMessageChange({ id: message._id, }); } -export async function executeSetReaction(userId: string, reaction: string, messageId: IMessage['_id'], shouldReact?: boolean) { - const user = await Users.findOneById(userId); +export async function executeSetReaction( + userId: string, + reaction: string, + messageParam: IMessage['_id'] | IMessage, + shouldReact?: boolean, +) { + // Check if the emoji is valid before proceeding + const reactionWithoutColons = reaction.replace(/:/g, ''); + reaction = `:${reactionWithoutColons}:`; + + if (!emoji.list[reaction] && (await EmojiCustom.countByNameOrAlias(reactionWithoutColons)) === 0) { + throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { + method: 'setReaction', + }); + } + const user = await Users.findOneById(userId); if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setReaction' }); } - const message = await Messages.findOneById(messageId); + const message = typeof messageParam === 'string' ? await Messages.findOneById(messageParam) : messageParam; if (!message) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } - const room = await Rooms.findOneById(message.rid); + const userAlreadyReacted = + message.reactions && Boolean(message.reactions[reaction]) && message.reactions[reaction].usernames.includes(user.username as string); + + // When shouldReact was not informed, toggle the reaction. + if (shouldReact === undefined) { + shouldReact = !userAlreadyReacted; + } + + if (userAlreadyReacted === shouldReact) { + return; + } + + const room = await Rooms.findOneById< + Pick + >(message.rid, { projection: { _id: 1, ro: 1, muted: 1, reactWhenReadOnly: 1, lastMessage: 1, t: 1, prid: 1, federated: 1 } }); if (!room) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } @@ -136,7 +149,7 @@ export async function executeSetReaction(userId: string, reaction: string, messa throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'setReaction' }); } - return setReaction(room, user, message, reaction, shouldReact); + return setReaction(room, user, message, reaction, userAlreadyReacted); } declare module '@rocket.chat/ddp-client' { diff --git a/apps/meteor/server/models/raw/EmojiCustom.ts b/apps/meteor/server/models/raw/EmojiCustom.ts index 300721f60d7b..ec3c6390f64d 100644 --- a/apps/meteor/server/models/raw/EmojiCustom.ts +++ b/apps/meteor/server/models/raw/EmojiCustom.ts @@ -72,4 +72,12 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto create(data: InsertionModel): Promise>> { return this.insertOne(data); } + + countByNameOrAlias(name: string): Promise { + const query = { + $or: [{ name }, { aliases: name }], + }; + + return this.countDocuments(query); + } } diff --git a/apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts b/apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts new file mode 100644 index 000000000000..e267825a6a18 --- /dev/null +++ b/apps/meteor/tests/unit/app/reactions/server/setReaction.spec.ts @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { beforeEach, describe, it } from 'mocha'; +import p from 'proxyquire'; +import sinon from 'sinon'; + +const meteorMethodsMock = sinon.stub(); +const emojiList: Record = {}; +const modelsMock = { + EmojiCustom: { + countByNameOrAlias: sinon.stub(), + }, + Users: { + findOneById: sinon.stub(), + }, + Messages: { + findOneById: sinon.stub(), + setReactions: sinon.stub(), + unsetReactions: sinon.stub(), + }, + Rooms: { + findOneById: sinon.stub(), + unsetReactionsInLastMessage: sinon.stub(), + setReactionsInLastMessage: sinon.stub(), + }, +}; +const canAccessRoomAsyncMock = sinon.stub(); +const isTheLastMessageMock = sinon.stub(); +const notifyOnMessageChangeMock = sinon.stub(); +const hasPermissionAsyncMock = sinon.stub(); +const i18nMock = { t: sinon.stub() }; +const callbacksRunMock = sinon.stub(); +const meteorErrorMock = class extends Error { + constructor(message: string) { + super(message); + } +}; + +const { removeUserReaction, executeSetReaction, setReaction } = p.noCallThru().load('../../../../../app/reactions/server/setReaction.ts', { + '@rocket.chat/models': modelsMock, + '@rocket.chat/core-services': { Message: { beforeReacted: sinon.stub() } }, + 'meteor/meteor': { Meteor: { methods: meteorMethodsMock, Error: meteorErrorMock } }, + '../../../lib/callbacks': { callbacks: { run: callbacksRunMock } }, + '../../../server/lib/i18n': { i18n: i18nMock }, + '../../authorization/server': { canAccessRoomAsync: canAccessRoomAsyncMock }, + '../../authorization/server/functions/hasPermission': { hasPermissionAsync: hasPermissionAsyncMock }, + '../../emoji/server': { emoji: { list: emojiList } }, + '../../lib/server/functions/isTheLastMessage': { isTheLastMessage: isTheLastMessageMock }, + '../../lib/server/lib/notifyListener': { + notifyOnMessageChange: notifyOnMessageChangeMock, + }, +}); + +describe('Reactions', () => { + describe('removeUserReaction', () => { + it('should return the message unmodified when no reactions exist', () => { + const message = {}; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result).to.equal(message); + }); + it('should remove the reaction from a message', () => { + const message = { + reactions: { + test: { + usernames: ['test', 'test2'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result.reactions.test.usernames).to.not.include('test'); + expect(result.reactions.test.usernames).to.include('test2'); + }); + it('should remove the reaction from a message when the user is the last one on the array', () => { + const message = { + reactions: { + test: { + usernames: ['test'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result.reactions.test).to.be.undefined; + }); + it('should remove username only from the reaction thats passed in', () => { + const message = { + reactions: { + test: { + usernames: ['test', 'test2'], + }, + other: { + usernames: ['test', 'test2'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test'); + expect(result.reactions.test.usernames).to.not.include('test'); + expect(result.reactions.test.usernames).to.include('test2'); + expect(result.reactions.other.usernames).to.include('test'); + expect(result.reactions.other.usernames).to.include('test2'); + }); + it('should do nothing if username is not in the reaction', () => { + const message = { + reactions: { + test: { + usernames: ['test', 'test2'], + }, + }, + }; + + const result = removeUserReaction(message as any, 'test', 'test3'); + expect(result.reactions.test.usernames).to.not.include('test3'); + expect(result.reactions.test.usernames).to.include('test'); + expect(result.reactions.test.usernames).to.include('test2'); + }); + }); + describe('executeSetReaction', () => { + beforeEach(() => { + modelsMock.EmojiCustom.countByNameOrAlias.reset(); + }); + it('should throw an error if reaction is not on emojione list', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(0); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-not-allowed'); + }); + it('should fail if user does not exist', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-invalid-user'); + }); + it('should fail if message does not exist', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-not-allowed'); + }); + it('should return nothing if user already reacted and its trying to react again', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + expect(await executeSetReaction('test', 'test', 'test', true)).to.be.undefined; + }); + it('should return nothing if user hasnt reacted and its trying to unreact', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['testxxxx'] } } }); + expect(await executeSetReaction('test', 'test', 'test', false)).to.be.undefined; + }); + it('should fail if room does not exist', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + modelsMock.Rooms.findOneById.resolves(undefined); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('error-not-allowed'); + }); + it('should fail if user doesnt have acccess to the room', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + modelsMock.Rooms.findOneById.resolves({ t: 'd' }); + canAccessRoomAsyncMock.resolves(false); + await expect(executeSetReaction('test', 'test', 'test')).to.be.rejectedWith('not-authorized'); + }); + it('should call setReaction with correct params', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Messages.findOneById.resolves({ reactions: { ':test:': { usernames: ['test'] } } }); + modelsMock.Rooms.findOneById.resolves({ t: 'c' }); + canAccessRoomAsyncMock.resolves(true); + + const res = await executeSetReaction('test', 'test', 'test'); + expect(res).to.be.undefined; + }); + it('should use the message from param when the type is not an string', async () => { + modelsMock.EmojiCustom.countByNameOrAlias.resolves(1); + modelsMock.Users.findOneById.resolves({ username: 'test' }); + modelsMock.Rooms.findOneById.resolves({ t: 'c' }); + canAccessRoomAsyncMock.resolves(true); + + await executeSetReaction('test', 'test', { reactions: { ':test:': { usernames: ['test'] } } }); + expect(modelsMock.Messages.findOneById.calledOnce).to.be.false; + }); + }); + describe('setReaction', () => { + beforeEach(() => { + canAccessRoomAsyncMock.reset(); + hasPermissionAsyncMock.reset(); + isTheLastMessageMock.reset(); + modelsMock.Messages.setReactions.reset(); + modelsMock.Rooms.setReactionsInLastMessage.reset(); + modelsMock.Rooms.unsetReactionsInLastMessage.reset(); + modelsMock.Messages.unsetReactions.reset(); + callbacksRunMock.reset(); + }); + it('should throw an error if user is muted from the room', async () => { + const room = { + muted: ['test'], + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + await expect(setReaction(room, user, message, ':test:')).to.be.rejectedWith('error-not-allowed'); + }); + it('should throw an error if room is readonly and cannot be reacted when readonly and user trying doesnt have permissions and user is not unmuted from room', async () => { + const room = { + ro: true, + reactWhenReadOnly: false, + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + canAccessRoomAsyncMock.resolves(false); + await expect(setReaction(room, user, message, ':test:')).to.be.rejectedWith("You can't send messages because the room is readonly."); + }); + it('should remove the user reaction if userAlreadyReacted is true and call unsetReaction if reaction is the last one on message', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.unsetReactions.calledWith(message._id)).to.be.true; + }); + it('should call Rooms.unsetReactionsInLastMessage when userAlreadyReacted is true and reaction is the last one on message', async () => { + const room = { + _id: 'test', + lastMessage: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + isTheLastMessageMock.resolves(true); + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.unsetReactions.calledWith(message._id)).to.be.true; + expect(modelsMock.Rooms.unsetReactionsInLastMessage.calledWith(room._id)).to.be.true; + }); + it('should update the reactions object when userAlreadyReacted is true and there is more reactions on message', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + ':test2:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test2:': { usernames: ['test'] } }))).to.be.true; + }); + it('should call Rooms.setReactionsInLastMessage when userAlreadyReacted is true and reaction is not the last one on message', async () => { + const room = { + _id: 'test', + lastMessage: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + ':test2:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + isTheLastMessageMock.resolves(true); + + await setReaction(room, user, message, reaction, true); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test2:': { usernames: ['test'] } }))).to.be.true; + expect(modelsMock.Rooms.setReactionsInLastMessage.calledWith(room._id, sinon.match({ ':test2:': { usernames: ['test'] } }))).to.be + .true; + }); + it('should call afterUnsetReaction callback when userAlreadyReacted is true', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, true); + expect( + callbacksRunMock.calledWith( + 'afterUnsetReaction', + sinon.match({ _id: 'test' }), + sinon.match({ user, reaction, shouldReact: false, oldMessage: message }), + ), + ).to.be.true; + }); + it('should set reactions when userAlreadyReacted is false', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + await setReaction(room, user, message, reaction, false); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test:': { usernames: ['test'] } }))).to.be.true; + }); + it('should properly add username to the list of reactions when userAlreadyReacted is false', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test2', + }; + const message = { + _id: 'test', + reactions: { + ':test:': { + usernames: ['test'], + }, + }, + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, false); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test:': { usernames: ['test', 'test2'] } }))).to.be + .true; + }); + it('should call Rooms.setReactionInLastMessage when userAlreadyReacted is false', async () => { + const room = { + _id: 'x5', + lastMessage: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + + isTheLastMessageMock.resolves(true); + + await setReaction(room, user, message, reaction, false); + expect(modelsMock.Messages.setReactions.calledWith(message._id, sinon.match({ ':test:': { usernames: ['test'] } }))).to.be.true; + expect(modelsMock.Rooms.setReactionsInLastMessage.calledWith(room._id, sinon.match({ ':test:': { usernames: ['test'] } }))).to.be + .true; + }); + it('should call afterSetReaction callback when userAlreadyReacted is false', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + + await setReaction(room, user, message, reaction, false); + expect( + callbacksRunMock.calledWith('afterSetReaction', sinon.match({ _id: 'test' }), sinon.match({ user, reaction, shouldReact: true })), + ).to.be.true; + }); + it('should return undefined on a successful reaction', async () => { + const room = { + _id: 'test', + }; + const user = { + username: 'test', + }; + const message = { + _id: 'test', + }; + const reaction = ':test:'; + + expect(await setReaction(room, user, message, reaction, false)).to.be.undefined; + }); + }); +}); diff --git a/packages/model-typings/src/models/IEmojiCustomModel.ts b/packages/model-typings/src/models/IEmojiCustomModel.ts index fba5f1c3ea10..30f0323c1ec7 100644 --- a/packages/model-typings/src/models/IEmojiCustomModel.ts +++ b/packages/model-typings/src/models/IEmojiCustomModel.ts @@ -10,4 +10,5 @@ export interface IEmojiCustomModel extends IBaseModel { setAliases(_id: string, aliases: string[]): Promise; setExtension(_id: string, extension: string): Promise; create(data: InsertionModel): Promise>>; + countByNameOrAlias(name: string): Promise; } From 274f4f58812cc0409c62380ca4292a64b6ab04b5 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Thu, 19 Sep 2024 21:15:14 -0300 Subject: [PATCH 31/57] feat: E2EE messages mentions (#32510) --- .changeset/late-planes-sniff.md | 7 ++ .../app/lib/server/methods/updateMessage.ts | 2 +- apps/meteor/app/mentions/server/Mentions.ts | 20 ++++-- apps/meteor/client/startup/e2e.ts | 22 ++++++ apps/meteor/server/settings/e2e.ts | 6 ++ apps/meteor/tests/e2e/e2e-encryption.spec.ts | 71 +++++++++++++++++++ .../page-objects/fragments/home-content.ts | 2 +- .../core-typings/src/IMessage/IMessage.ts | 1 + packages/i18n/src/locales/en.i18n.json | 2 + 9 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 .changeset/late-planes-sniff.md diff --git a/.changeset/late-planes-sniff.md b/.changeset/late-planes-sniff.md new file mode 100644 index 000000000000..d702a938da78 --- /dev/null +++ b/.changeset/late-planes-sniff.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": patch +"@rocket.chat/i18n": patch +--- + +Added a new setting to enable mentions in end to end encrypted channels diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 8cebe563cd23..c03208a438e9 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -10,7 +10,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { settings } from '../../../settings/server'; import { updateMessage } from '../functions/updateMessage'; -const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content']; +const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions']; export async function executeUpdateMessage( uid: IUser['_id'], diff --git a/apps/meteor/app/mentions/server/Mentions.ts b/apps/meteor/app/mentions/server/Mentions.ts index 9eda56fea21c..779af2087932 100644 --- a/apps/meteor/app/mentions/server/Mentions.ts +++ b/apps/meteor/app/mentions/server/Mentions.ts @@ -2,7 +2,7 @@ * Mentions is a named function that will process Mentions * @param {Object} message - The message object */ -import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import { isE2EEMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; import { type MentionsParserArgs, MentionsParser } from '../lib/MentionsParser'; @@ -43,8 +43,13 @@ export class MentionsServer extends MentionsParser { }); } - async getUsersByMentions({ msg, rid, u: sender }: Pick): Promise { - const mentions = this.getUserMentions(msg); + async getUsersByMentions(message: IMessage): Promise { + const { msg, rid, u: sender, e2eMentions }: Pick = message; + + const mentions = + isE2EEMessage(message) && e2eMentions?.e2eUserMentions && e2eMentions?.e2eUserMentions.length > 0 + ? e2eMentions?.e2eUserMentions + : this.getUserMentions(msg); const mentionsAll: { _id: string; username: string }[] = []; const userMentions = []; @@ -67,8 +72,13 @@ export class MentionsServer extends MentionsParser { return [...mentionsAll, ...(userMentions.length ? await this.getUsers(userMentions) : [])]; } - async getChannelbyMentions({ msg }: Pick) { - const channels = this.getChannelMentions(msg); + async getChannelbyMentions(message: IMessage) { + const { msg, e2eMentions }: Pick = message; + + const channels = + isE2EEMessage(message) && e2eMentions?.e2eChannelMentions && e2eMentions?.e2eChannelMentions.length > 0 + ? e2eMentions?.e2eChannelMentions + : this.getChannelMentions(msg); return this.getChannels(channels.map((c) => c.trim().substr(1))); } diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index de615e8f45de..e45b62563726 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -5,6 +5,7 @@ import { Tracker } from 'meteor/tracker'; import { E2EEState } from '../../app/e2e/client/E2EEState'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; +import { MentionsParser } from '../../app/mentions/lib/MentionsParser'; import { ChatRoom } from '../../app/models/client'; import { settings } from '../../app/settings/client'; import { onClientBeforeSendMessage } from '../lib/onClientBeforeSendMessage'; @@ -88,6 +89,27 @@ Meteor.startup(() => { return message; } + const mentionsEnabled = settings.get('E2E_Enabled_Mentions'); + + if (mentionsEnabled) { + const me = Meteor.user()?.username || ''; + const pattern = settings.get('UTF8_User_Names_Validation'); + const useRealName = settings.get('UI_Use_Real_Name'); + + const mentions = new MentionsParser({ + pattern: () => pattern, + useRealName: () => useRealName, + me: () => me, + }); + + const e2eMentions: IMessage['e2eMentions'] = { + e2eUserMentions: mentions.getUserMentions(message.msg), + e2eChannelMentions: mentions.getChannelMentions(message.msg), + }; + + message.e2eMentions = e2eMentions; + } + // Should encrypt this message. return e2eRoom.encryptMessage(message); }); diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index 6f22784f1709..c8a69757128b 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -35,4 +35,10 @@ export const createE2ESettings = () => public: true, enableQuery: { _id: 'E2E_Enable', value: true }, }); + + await this.add('E2E_Enabled_Mentions', false, { + type: 'boolean', + public: true, + enableQuery: { _id: 'E2E_Enable', value: true }, + }); }); diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index 8c6297e9975d..ad98df1aaa53 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -133,11 +133,13 @@ test.describe.serial('e2e-encryption', () => { test.beforeAll(async ({ api }) => { expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200); + expect((await api.post('/settings/E2E_Enabled_Mentions', { value: true })).status()).toBe(200); }); test.afterAll(async ({ api }) => { expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Enabled_Mentions', { value: false })).status()).toBe(200); }); test('expect create a private channel encrypted and send an encrypted message', async ({ page }) => { @@ -265,6 +267,75 @@ test.describe.serial('e2e-encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); }); + test('expect create a encrypted private channel and mention user', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('hello @user1'); + + const userMention = await page.getByRole('button', { + name: 'user1', + }); + + await expect(userMention).toBeVisible(); + }); + + test('expect create a encrypted private channel, mention a channel and navigate to it', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('Are you in the #general channel?'); + + const channelMention = await page.getByRole('button', { + name: 'general', + }); + + await expect(channelMention).toBeVisible(); + + await channelMention.click(); + + await expect(page).toHaveURL(`/channel/general`); + }); + + test('expect create a encrypted private channel, mention a channel and user', async ({ page }) => { + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.createEncryptedChannel(channelName); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('Are you in the #general channel, @user1 ?'); + + const channelMention = await page.getByRole('button', { + name: 'general', + }); + + const userMention = await page.getByRole('button', { + name: 'user1', + }); + + await expect(userMention).toBeVisible(); + await expect(channelMention).toBeVisible(); + }); + test('should encrypted field be available on edit room', async ({ page }) => { const channelName = faker.string.uuid(); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 9d5e2081ca93..519d9a4102aa 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -85,7 +85,7 @@ export class HomeContent { await this.joinRoomIfNeeded(); await this.page.waitForSelector('[name="msg"]:not([disabled])'); await this.page.locator('[name="msg"]').fill(text); - await this.page.keyboard.press('Enter'); + await this.page.getByLabel('Send').click(); } async dispatchSlashCommand(text: string): Promise { diff --git a/packages/core-typings/src/IMessage/IMessage.ts b/packages/core-typings/src/IMessage/IMessage.ts index 205cbaccd466..6c5511966ac8 100644 --- a/packages/core-typings/src/IMessage/IMessage.ts +++ b/packages/core-typings/src/IMessage/IMessage.ts @@ -170,6 +170,7 @@ export interface IMessage extends IRocketChatRecord { tcount?: number; t?: MessageTypesValues; e2e?: 'pending' | 'done'; + e2eMentions?: { e2eUserMentions?: string[]; e2eChannelMentions?: string[] }; otrAck?: string; urls?: MessageUrl[]; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 728ca256952d..8ce6bea2e117 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1805,6 +1805,8 @@ "E2E_Enabled": "E2E Enabled", "E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default", "E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default", + "E2E_Enabled_Mentions": "Mentions", + "E2E_Enabled_Mentions_Description": "Notify people, and highlight user, channel, and team mentions in encrypted content.", "E2E_Enable_Encrypt_Files": "Encrypt files", "E2E_Enable_Encrypt_Files_Description": "Encrypt files sent inside encrypted rooms. Check for possible conflicts in [file upload settings.](admin/settings/FileUpload)", "E2E_Encryption_Password_Change": "Change Encryption Password", From 9c119a0abd9963b5ef22f64240cb118e65498c7e Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:16:24 -0500 Subject: [PATCH 32/57] fix: markdown inconsistency with bold and italics (#33157) --- .changeset/sweet-nails-grin.md | 5 + .../client/components/MarkdownText.spec.tsx | 92 +++++++++++++++++++ .../meteor/client/components/MarkdownText.tsx | 14 ++- 3 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 .changeset/sweet-nails-grin.md create mode 100644 apps/meteor/client/components/MarkdownText.spec.tsx diff --git a/.changeset/sweet-nails-grin.md b/.changeset/sweet-nails-grin.md new file mode 100644 index 000000000000..de240bfc0e3f --- /dev/null +++ b/.changeset/sweet-nails-grin.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixed inconsistency between the markdown parser from the composer and the rest of the application when using bold and italics in a text. diff --git a/apps/meteor/client/components/MarkdownText.spec.tsx b/apps/meteor/client/components/MarkdownText.spec.tsx new file mode 100644 index 000000000000..86ebadad8463 --- /dev/null +++ b/apps/meteor/client/components/MarkdownText.spec.tsx @@ -0,0 +1,92 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import MarkdownText from './MarkdownText'; + +import '@testing-library/jest-dom'; + +const normalizeHtml = (html: any) => { + return html.replace(/\s+/g, ' ').trim(); +}; + +const markdownText = ` + # Heading 1 + **Paragraph text**: *Bold with one asterisk* **Bold with two asterisks** Lorem ipsum dolor sit amet, consectetur adipiscing elit. + ## Heading 2 + _Italic Text_: _Italic with one underscore_ __Italic with two underscores__ Lorem ipsum dolor sit amet, consectetur adipiscing elit. + ### Heading 3 + Lists, Links and elements + **Unordered List** + - List Item 1 + - List Item 2 + - List Item 3 + - List Item 4 + **Ordered List** + 1. List Item 1 + 2. List Item 2 + 3. List Item 3 + 4. List Item 4 + **Links:** + [Rocket.Chat](rocket.chat) + gabriel.engel@rocket.chat + +55991999999 + \`Inline code\` + \`\`\`typescript + const test = 'this is code' + \`\`\` +`; + +it('should render html elements as expected using default parser', async () => { + const { container } = render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + const normalizedHtml = normalizeHtml(container.innerHTML); + + expect(normalizedHtml).toContain('

    Heading 1

    '); + expect(normalizedHtml).toContain( + 'Paragraph text: Bold with one asterisk Bold with two asterisks Lorem ipsum dolor sit amet', + ); + expect(normalizedHtml).toContain('

    Heading 2

    '); + expect(normalizedHtml).toContain( + 'Italic Text: Italic with one underscore Italic with two underscores Lorem ipsum dolor sit amet', + ); + expect(normalizedHtml).toContain('

    Heading 3

    '); + expect(normalizedHtml).toContain('
    • List Item 1
    • List Item 2
    • List Item 3
    • List Item 4'); + expect(normalizedHtml).toContain('
      1. List Item 1
      2. List Item 2
      3. List Item 3
      4. List Item 4'); + expect(normalizedHtml).toContain('Rocket.Chat'); + expect(normalizedHtml).toContain('gabriel.engel@rocket.chat'); + expect(normalizedHtml).toContain('+55991999999'); + expect(normalizedHtml).toContain('Inline code'); + expect(normalizedHtml).toContain('
        const test = \'this is code\' 
        '); +}); + +it('should render html elements as expected using inline parser', async () => { + const { container } = render(, { + wrapper: mockAppRoot().build(), + legacyRoot: true, + }); + + const normalizedHtml = normalizeHtml(container.innerHTML); + + expect(normalizedHtml).toContain('# Heading 1'); + expect(normalizedHtml).toContain( + 'Bold with one asterisk Bold with two asterisks Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + ); + expect(normalizedHtml).toContain('## Heading 2'); + expect(normalizedHtml).toContain( + 'Italic Text: Italic with one underscore Italic with two underscores Lorem ipsum dolor sit amet', + ); + expect(normalizedHtml).toContain('### Heading 3'); + expect(normalizedHtml).toContain('Unordered List - List Item 1 - List Item 2 - List Item 3 - List Item 4'); + expect(normalizedHtml).toContain('Ordered List 1. List Item 1 2. List Item 2 3. List Item 3 4. List Item 4'); + expect(normalizedHtml).toContain(`Rocket.Chat`); + expect(normalizedHtml).toContain( + `gabriel.engel@rocket.chat`, + ); + expect(normalizedHtml).toContain('+55991999999'); + expect(normalizedHtml).toContain('Inline code'); + expect(normalizedHtml).toContain(`typescript const test = 'this is code'`); +}); diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index 6426b24810ee..0b7d2efa780e 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -20,12 +20,18 @@ const documentRenderer = new marked.Renderer(); const inlineRenderer = new marked.Renderer(); const inlineWithoutBreaks = new marked.Renderer(); -marked.Lexer.rules.gfm = { - ...marked.Lexer.rules.gfm, - strong: /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, - em: /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/, +const walkTokens = (token: marked.Token) => { + const boldPattern = /^\*[^*]+\*$|^\*\*[^*]+\*\*$/; + const italicPattern = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/; + if (boldPattern.test(token.raw)) { + token.type = 'strong'; + } else if (italicPattern.test(token.raw)) { + token.type = 'em'; + } }; +marked.use({ walkTokens }); + const linkMarked = (href: string | null, _title: string | null, text: string): string => `${text} `; const paragraphMarked = (text: string): string => text; From 599762739a71ee6b0f3a090db571a47498876a86 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:20:48 -0300 Subject: [PATCH 33/57] fix: conference calls are shown as "not answered" after they end (#33179) --- .changeset/five-coats-rhyme.md | 5 ++++ apps/uikit-playground/package.json | 1 + apps/uikit-playground/vite.config.ts | 4 +-- .../VideoConferenceBlock.tsx | 25 +++++++++++++------ 4 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 .changeset/five-coats-rhyme.md diff --git a/.changeset/five-coats-rhyme.md b/.changeset/five-coats-rhyme.md new file mode 100644 index 000000000000..c5359e3c978a --- /dev/null +++ b/.changeset/five-coats-rhyme.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/fuselage-ui-kit': patch +--- + +Fixed an error that incorrectly showed conference calls as not answered after they ended diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 791526c46f9d..4cca93dc00e9 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -15,6 +15,7 @@ "@codemirror/lang-json": "^6.0.1", "@codemirror/tooltip": "^0.19.16", "@lezer/highlight": "^1.1.6", + "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/fuselage": "^0.59.0", "@rocket.chat/fuselage-hooks": "^0.33.1", diff --git a/apps/uikit-playground/vite.config.ts b/apps/uikit-playground/vite.config.ts index 61a5ab30e647..4d382d652859 100644 --- a/apps/uikit-playground/vite.config.ts +++ b/apps/uikit-playground/vite.config.ts @@ -7,11 +7,11 @@ export default defineConfig(() => ({ esbuild: {}, plugins: [react()], optimizeDeps: { - include: ['@rocket.chat/ui-contexts', '@rocket.chat/message-parser'], + include: ['@rocket.chat/ui-contexts', '@rocket.chat/message-parser', '@rocket.chat/core-typings'], }, build: { commonjsOptions: { - include: [/ui-contexts/, /message-parser/, /node_modules/], + include: [/ui-contexts/, /core-typings/, /message-parser/, /node_modules/], }, }, })); diff --git a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx index 969ad0af1d7c..7125dbbf1bc4 100644 --- a/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx +++ b/packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx @@ -1,3 +1,4 @@ +import { VideoConferenceStatus } from '@rocket.chat/core-typings'; import { useGoToRoom, useTranslation, @@ -133,9 +134,14 @@ const VideoConferenceBlock = ({ {isUserCaller ? t('Call_again') : t('Call_back')} - - {t('Call_was_not_answered')} - + {[ + VideoConferenceStatus.EXPIRED, + VideoConferenceStatus.DECLINED, + ].includes(data.status) && ( + + {t('Call_was_not_answered')} + + )} )} {data.type !== 'direct' && @@ -151,16 +157,21 @@ const VideoConferenceBlock = ({ ) : ( - - {t('Call_was_not_answered')} - + [ + VideoConferenceStatus.EXPIRED, + VideoConferenceStatus.DECLINED, + ].includes(data.status) && ( + + {t('Call_was_not_answered')} + + ) ))} ); } - if (data.type === 'direct' && data.status === 0) { + if (data.type === 'direct' && data.status === VideoConferenceStatus.CALLING) { return ( From 027183fc3e2ffe2b2c2b0875e43dd636aed99ae4 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Fri, 20 Sep 2024 00:47:27 +0000 Subject: [PATCH 34/57] Release 6.13.0-rc.0 --- .changeset/pre.json | 100 +++++++++++++++++ apps/meteor/CHANGELOG.md | 101 ++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 16 +++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 13 +++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 13 +++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 14 +++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 14 +++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 13 +++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 13 +++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 12 +++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 9 ++ ee/packages/license/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 14 +++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 9 ++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 11 ++ ee/packages/presence/package.json | 2 +- ee/packages/ui-theming/CHANGELOG.md | 6 ++ ee/packages/ui-theming/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 10 ++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 10 ++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 20 ++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 17 +++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 10 ++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 11 ++ packages/ddp-client/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 21 ++++ packages/fuselage-ui-kit/package.json | 8 +- packages/gazzodown/CHANGELOG.md | 16 +++ packages/gazzodown/package.json | 8 +- packages/i18n/CHANGELOG.md | 14 +++ packages/i18n/package.json | 2 +- packages/instance-status/CHANGELOG.md | 9 ++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 15 +++ packages/livechat/package.json | 2 +- packages/message-parser/CHANGELOG.md | 6 ++ packages/message-parser/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 ++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 19 ++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 13 +++ packages/models/package.json | 2 +- packages/peggy-loader/CHANGELOG.md | 6 ++ packages/peggy-loader/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 24 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 13 +++ packages/ui-avatar/package.json | 4 +- packages/ui-client/CHANGELOG.md | 18 ++++ packages/ui-client/package.json | 6 +- packages/ui-composer/CHANGELOG.md | 6 ++ packages/ui-composer/package.json | 2 +- packages/ui-contexts/CHANGELOG.md | 12 +++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 14 +++ packages/ui-video-conf/package.json | 6 +- packages/web-ui-registration/CHANGELOG.md | 9 ++ packages/web-ui-registration/package.json | 4 +- yarn.lock | 1 + 80 files changed, 727 insertions(+), 52 deletions(-) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000000..6976ce2b60aa --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,100 @@ +{ + "mode": "pre", + "tag": "rc", + "initialVersions": { + "@rocket.chat/meteor": "6.13.0-develop", + "rocketchat-services": "1.3.3", + "@rocket.chat/uikit-playground": "0.4.0", + "@rocket.chat/account-service": "0.4.6", + "@rocket.chat/authorization-service": "0.4.6", + "@rocket.chat/ddp-streamer": "0.3.6", + "@rocket.chat/omnichannel-transcript": "0.4.6", + "@rocket.chat/presence-service": "0.4.6", + "@rocket.chat/queue-worker": "0.4.6", + "@rocket.chat/stream-hub-service": "0.4.6", + "@rocket.chat/license": "0.2.6", + "@rocket.chat/omnichannel-services": "0.3.3", + "@rocket.chat/pdf-worker": "0.2.3", + "@rocket.chat/presence": "0.2.6", + "@rocket.chat/ui-theming": "0.2.1", + "@rocket.chat/account-utils": "0.0.2", + "@rocket.chat/agenda": "0.1.0", + "@rocket.chat/api-client": "0.2.6", + "@rocket.chat/apps": "0.1.6", + "@rocket.chat/base64": "1.0.13", + "@rocket.chat/cas-validate": "0.0.2", + "@rocket.chat/core-services": "0.6.0", + "@rocket.chat/core-typings": "6.13.0-develop", + "@rocket.chat/cron": "0.1.6", + "@rocket.chat/ddp-client": "0.3.6", + "@rocket.chat/eslint-config": "0.7.0", + "@rocket.chat/favicon": "0.0.2", + "@rocket.chat/fuselage-ui-kit": "10.0.0", + "@rocket.chat/gazzodown": "10.0.0", + "@rocket.chat/i18n": "0.7.0", + "@rocket.chat/instance-status": "0.1.6", + "@rocket.chat/jest-presets": "0.0.1", + "@rocket.chat/jwt": "0.1.1", + "@rocket.chat/livechat": "1.19.3", + "@rocket.chat/log-format": "0.0.2", + "@rocket.chat/logger": "0.0.2", + "@rocket.chat/message-parser": "0.31.29", + "@rocket.chat/mock-providers": "0.1.2", + "@rocket.chat/model-typings": "0.7.0", + "@rocket.chat/models": "0.2.3", + "@rocket.chat/poplib": "0.0.2", + "@rocket.chat/password-policies": "0.0.2", + "@rocket.chat/patch-injection": "0.0.1", + "@rocket.chat/peggy-loader": "0.31.25", + "@rocket.chat/random": "1.2.2", + "@rocket.chat/release-action": "2.2.3", + "@rocket.chat/release-changelog": "0.1.0", + "@rocket.chat/rest-typings": "6.13.0-develop", + "@rocket.chat/server-cloud-communication": "0.0.2", + "@rocket.chat/server-fetch": "0.0.3", + "@rocket.chat/sha256": "1.0.10", + "@rocket.chat/tools": "0.2.2", + "@rocket.chat/ui-avatar": "6.0.0", + "@rocket.chat/ui-client": "10.0.0", + "@rocket.chat/ui-composer": "0.2.1", + "@rocket.chat/ui-contexts": "10.0.0", + "@rocket.chat/ui-kit": "0.36.1", + "@rocket.chat/ui-video-conf": "10.0.0", + "@rocket.chat/web-ui-registration": "10.0.0" + }, + "changesets": [ + "brown-singers-appear", + "cyan-ladybugs-thank", + "dirty-stingrays-beg", + "five-coats-rhyme", + "four-cherries-kneel", + "great-humans-live", + "healthy-rivers-nail", + "heavy-snails-help", + "hot-balloons-travel", + "khaki-cameras-glow", + "kind-llamas-grin", + "late-planes-sniff", + "many-balloons-scream", + "many-rules-shout", + "mighty-drinks-hide", + "nasty-tools-enjoy", + "pink-swans-teach", + "quiet-cherries-punch", + "rich-toes-bow", + "rotten-rabbits-brush", + "short-drinks-itch", + "sixty-spoons-own", + "small-crabs-travel", + "soft-mirrors-remember", + "spicy-rocks-burn", + "strong-grapes-brake", + "stupid-pigs-share", + "sweet-nails-grin", + "tame-mayflies-press", + "tiny-geckos-kiss", + "wet-hats-walk", + "wise-avocados-taste", + "witty-lemons-type" + ] +} diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index 97a246abaca6..dd941b905c3c 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,106 @@ # @rocket.chat/meteor +## 6.13.0-rc.0 + +### Minor Changes + +- ([#33156](https://github.com/RocketChat/Rocket.Chat/pull/33156)) added `sidepanelNavigation` to feature preview list + +- ([#32682](https://github.com/RocketChat/Rocket.Chat/pull/32682)) Added support for specifying a unit on departments' creation and update + +- ([#33139](https://github.com/RocketChat/Rocket.Chat/pull/33139)) Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms. + However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting. +- ([#32729](https://github.com/RocketChat/Rocket.Chat/pull/32729)) Implemented "omnichannel/contacts.update" endpoint to update contacts + +- ([#32510](https://github.com/RocketChat/Rocket.Chat/pull/32510)) Added a new setting to enable mentions in end to end encrypted channels + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +- ([#33011](https://github.com/RocketChat/Rocket.Chat/pull/33011)) Return `parent` and `team` information when calling `rooms.info` endpoint + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +- ([#33177](https://github.com/RocketChat/Rocket.Chat/pull/33177)) New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. + +- ([#33114](https://github.com/RocketChat/Rocket.Chat/pull/33114)) Wraps some room settings in an accordion advanced settings section in room edit contextual bar to improve organization + +- ([#33160](https://github.com/RocketChat/Rocket.Chat/pull/33160)) Implemented sending email via apps + +- ([#32945](https://github.com/RocketChat/Rocket.Chat/pull/32945)) Added a new setting which allows workspace admins to disable email two factor authentication for SSO (OAuth) users. If enabled, SSO users won't be asked for email two factor authentication. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +- ([#33317](https://github.com/RocketChat/Rocket.Chat/pull/33317)) Fixed error during sendmessage client stub + +- ([#33211](https://github.com/RocketChat/Rocket.Chat/pull/33211)) Allow to use the token from `room.v` when requesting transcript instead of visitor token. Visitors may change their tokens at any time, rendering old conversations impossible to access for them (or for APIs depending on token) as the visitor token won't match the `room.v` token. + +- ([#33298](https://github.com/RocketChat/Rocket.Chat/pull/33298)) Fixed a Federation callback not awaiting db call + +- ([#32939](https://github.com/RocketChat/Rocket.Chat/pull/32939)) Fixed issue where when you marked a room as unread and you were part of it, sometimes it would mark it as read right after + +- ([#33197](https://github.com/RocketChat/Rocket.Chat/pull/33197)) Fixes an issue where the retention policy warning keep displaying even if the retention is disabled inside the room + +- ([#33321](https://github.com/RocketChat/Rocket.Chat/pull/33321)) Changed the contextualbar behavior based on chat size instead the viewport + +- ([#33246](https://github.com/RocketChat/Rocket.Chat/pull/33246)) Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) + +- ([#32999](https://github.com/RocketChat/Rocket.Chat/pull/32999)) Fixes multiple selection for MultiStaticSelectElement in UiKit + +- ([#33155](https://github.com/RocketChat/Rocket.Chat/pull/33155)) Fixed a code issue on NPS service. It was passing `startAt` as the expiration date when creating a banner. + +- ([#33237](https://github.com/RocketChat/Rocket.Chat/pull/33237)) fixed retention policy max age settings not being respected after upgrade + +- ([#33216](https://github.com/RocketChat/Rocket.Chat/pull/33216)) Prevented uiInteraction to subscribe multiple times + +- ([#33295](https://github.com/RocketChat/Rocket.Chat/pull/33295)) Resolves the issue where outgoing integrations failed to trigger after the version 6.12.0 upgrade by correcting the parameter order from the `afterSaveMessage` callback to listener functions. This ensures the correct room information is passed, restoring the functionality of outgoing webhooks, IRC bridge, Autotranslate, and Engagement Dashboard. + +- ([#33193](https://github.com/RocketChat/Rocket.Chat/pull/33193)) Fixed avatar blob image setting in setUserAvatar method by correcting service handling logic. + +- ([#33209](https://github.com/RocketChat/Rocket.Chat/pull/33209)) Fixed `LivechatSessionTaken` webhook event being called without the `agent` param, which represents the agent serving the room. + +- ([#33296](https://github.com/RocketChat/Rocket.Chat/pull/33296)) Fixed remaining direct references to external user avatar URLs + + Fixed local avatars having priority over external provider + + It mainly corrects the behavior of E2E encryption messages and desktop notifications. + +- ([#33157](https://github.com/RocketChat/Rocket.Chat/pull/33157) by [@csuadev](https://github.com/csuadev)) Fixed inconsistency between the markdown parser from the composer and the rest of the application when using bold and italics in a text. + +- ([#33181](https://github.com/RocketChat/Rocket.Chat/pull/33181)) Fixed issue that caused an infinite loading state when uploading a private app to Rocket.Chat + +- ([#33158](https://github.com/RocketChat/Rocket.Chat/pull/33158)) Fixes an issue where multi-step modals were closing unexpectedly + +-
        Updated dependencies [bb94c9c67a, 9a38c8e13f, 599762739a, 7c14fd1a80, 9eaefdc892, 274f4f5881, cd0d50016e, 78e6ba4820, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 0f21fa01a3, 12d6307998]: + + - @rocket.chat/ui-client@11.0.0-rc.0 + - @rocket.chat/i18n@0.8.0-rc.0 + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-video-conf@11.0.0-rc.0 + - @rocket.chat/ui-composer@0.3.0-rc.0 + - @rocket.chat/gazzodown@11.0.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/web-ui-registration@11.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.4-rc.0 + - @rocket.chat/apps@0.1.7-rc.0 + - @rocket.chat/presence@0.2.7-rc.0 + - @rocket.chat/api-client@0.2.7-rc.0 + - @rocket.chat/license@0.2.7-rc.0 + - @rocket.chat/pdf-worker@0.2.4-rc.0 + - @rocket.chat/cron@0.1.7-rc.0 + - @rocket.chat/instance-status@0.1.7-rc.0 + - @rocket.chat/server-cloud-communication@0.0.2 +
        + ## 6.12.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index cb3ad01b882a..fc41ef05cacd 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-develop" + "version": "6.13.0-rc.0" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index 1d6e058d7597..6d9cafc01648 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,19 @@ # rocketchat-services +## 1.3.4-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 1.3.3 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 43659382eb67..82a55122337e 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.3.3", + "version": "1.3.4-rc.0", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9ee7e48d1794..a09dcb657466 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "private": true, "author": { "name": "Rocket.Chat", diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index 63a8eb47d7c6..27cdcb061719 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,21 @@ # @rocket.chat/uikit-playground +## 0.5.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies [599762739a, 274f4f5881, cd0d50016e, 78e6ba4820, 927710d778, 12d6307998]: + + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 0.4.0 ### Minor Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 4cca93dc00e9..48c099b8d9d1 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.4.0", + "version": "0.5.0-rc.0", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 8d306d7b1c17..40d597827934 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/account-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b9e45ed14eda..8438504f62f6 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 494c052e8cf8..70479a00abdc 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/authorization-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index b7912b8bc94d..19cbbd4035cf 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 97565b8bde88..2e13a89d03ec 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ddp-streamer +## 0.3.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/instance-status@0.1.7-rc.0 +
        + ## 0.3.6 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 87029e4b8993..d3ae93df7264 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.6", + "version": "0.3.7-rc.0", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index 21fc879f9fd9..b29eac976532 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-transcript +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.4-rc.0 + - @rocket.chat/pdf-worker@0.2.4-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 27290070b653..f28dd6a6d783 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index a1ed37bb6fde..a5ad6f38cfc5 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/presence-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/presence@0.2.7-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 5968631341b0..898de184e655 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 16dc2590f38b..7517afb9c624 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/queue-worker +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/omnichannel-services@0.3.4-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b3d8b0aff94e..c661ab840b9b 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index ccbb83e7de0b..3469df54a291 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/stream-hub-service +## 0.4.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.4.6 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 9fd5cf0586f6..aecc6e9a9e99 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.6", + "version": "0.4.7-rc.0", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 181b6dfb0e7f..277ae4cbed4a 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 0.2.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.2.6 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 3eceb35e27ff..4adcdae3826f 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "0.2.6", + "version": "0.2.7-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index f54493a31cd1..9ee4cf76a087 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.4-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 + - @rocket.chat/pdf-worker@0.2.4-rc.0 +
        + ## 0.3.3 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 696ac6fee640..f6d4f34e8e1a 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.3", + "version": "0.3.4-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index ccc01c92e84f..aa927a7b7c26 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.2.4-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.2.3 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 234463087a4e..824f196df01c 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.3", + "version": "0.2.4-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index a4effccb5f2b..b4d74639e58f 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/core-services@0.7.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.2.6 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 4d599562b278..80c6afe06875 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.6", + "version": "0.2.7-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/ee/packages/ui-theming/CHANGELOG.md b/ee/packages/ui-theming/CHANGELOG.md index 3a4956d146c3..b25ae24af33d 100644 --- a/ee/packages/ui-theming/CHANGELOG.md +++ b/ee/packages/ui-theming/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-theming +## 0.3.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + ## 0.2.1 ### Patch Changes diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index d6e3e93c01a4..0548f235c3fd 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-theming", - "version": "0.2.1", + "version": "0.3.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", diff --git a/package.json b/package.json index f3e3ce3d3e91..2f89cab55d2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 50096d81e901..6ba6da8d6cb3 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.2.6 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index b8d9ac155144..7e08a4d4f4d7 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.6", + "version": "0.2.7-rc.0", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index adb1b989bad0..4c7d0df77a29 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.1.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 274f4f5881, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.1.6 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index ba7d008543b7..b50ad6b74309 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.1.6", + "version": "0.1.7-rc.0", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index a73f804408ba..58b5d0df4f6e 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,25 @@ # @rocket.chat/core-services +## 0.7.0-rc.0 + +### Minor Changes + +- ([#33011](https://github.com/RocketChat/Rocket.Chat/pull/33011)) Return `parent` and `team` information when calling `rooms.info` endpoint + +- ([#33177](https://github.com/RocketChat/Rocket.Chat/pull/33177)) New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 79c16d315a, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.6.0 ### Minor Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 74250ffa8c16..7326f3cd3617 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.6.0", + "version": "0.7.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index ac2c861590c0..1898fe2767c1 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,22 @@ # @rocket.chat/core-typings +## 6.13.0-rc.0 + +### Minor Changes + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +- ([#32510](https://github.com/RocketChat/Rocket.Chat/pull/32510)) Added a new setting to enable mentions in end to end encrypted channels + +-
        Updated dependencies [79c16d315a]: + + - @rocket.chat/message-parser@0.31.30-rc.0 +
        + ## 6.12.0 ### Minor Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 86123a156e75..2c5cb3f64a2d 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 9c0c43c6f16e..7dbf3c56c145 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.1.6 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index aa1226271bba..a9f6a0583f78 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.6", + "version": "0.1.7-rc.0", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index 29e062a1374d..82ccc01d5a24 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/api-client@0.2.7-rc.0 +
        + ## 0.3.6 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 0fc47dc9a122..5b78e5ebf1e9 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.6", + "version": "0.3.7-rc.0", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 41cc87932393..085dedb33bd1 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,26 @@ # Change Log +## 11.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +- ([#33179](https://github.com/RocketChat/Rocket.Chat/pull/33179)) Fixed an error that incorrectly showed conference calls as not answered after they ended + +- ([#32999](https://github.com/RocketChat/Rocket.Chat/pull/32999)) Fixes multiple selection for MultiStaticSelectElement in UiKit + +-
        Updated dependencies [274f4f5881, cd0d50016e, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ui-video-conf@11.0.0-rc.0 + - @rocket.chat/gazzodown@11.0.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 80874491a951..8fb0ca254d68 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "10.0.0", + "version": "11.0.0-rc.0", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -50,10 +50,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-avatar": "7.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "*", + "@rocket.chat/ui-video-conf": "11.0.0-rc.0", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 74cd44cc291c..5216831e8365 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,21 @@ # @rocket.chat/gazzodown +## 11.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies [bb94c9c67a, 274f4f5881, cd0d50016e, 79c16d315a, 927710d778, 12d6307998]: + + - @rocket.chat/ui-client@11.0.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index c2fbf136b2d6..47bf2782226e 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -72,10 +72,10 @@ "@rocket.chat/css-in-js": "*", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-tokens": "*", - "@rocket.chat/message-parser": "0.31.29", + "@rocket.chat/message-parser": "0.31.30-rc.0", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-client": "11.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 8a7e4f7317b9..27bb4b471c66 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/i18n +## 0.8.0-rc.0 + +### Minor Changes + +- ([#33156](https://github.com/RocketChat/Rocket.Chat/pull/33156)) added `sidepanelNavigation` to feature preview list + +- ([#33139](https://github.com/RocketChat/Rocket.Chat/pull/33139)) Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms. + However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting. +- ([#32945](https://github.com/RocketChat/Rocket.Chat/pull/32945)) Added a new setting which allows workspace admins to disable email two factor authentication for SSO (OAuth) users. If enabled, SSO users won't be asked for email two factor authentication. + +### Patch Changes + +- ([#32510](https://github.com/RocketChat/Rocket.Chat/pull/32510)) Added a new setting to enable mentions in end to end encrypted channels + ## 0.7.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 7c66e102d578..fe98e3b1f7fc 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "0.7.0", + "version": "0.8.0-rc.0", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index bebd0fd56b85..e98235550ef8 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.7-rc.0 + +### Patch Changes + +-
        Updated dependencies [927710d778]: + + - @rocket.chat/models@0.3.0-rc.0 +
        + ## 0.1.6 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 5ea3b2cb6e0d..d092d649c80f 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.6", + "version": "0.1.7-rc.0", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 598fae50d68d..3c9bde71c23a 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,20 @@ # @rocket.chat/livechat Change Log +## 1.20.0-rc.0 + +### Minor Changes + +- ([#33139](https://github.com/RocketChat/Rocket.Chat/pull/33139)) Added new setting `Allow visitors to finish conversations` that allows admins to decide if omnichannel visitors can close a conversation or not. This doesn't affect agent's capabilities of room closing, neither apps using the livechat bridge to close rooms. + However, if currently your integration relies on `livechat/room.close` endpoint for closing conversations, it's advised to use the authenticated version `livechat/room.closeByUser` of it before turning off this setting. + +### Patch Changes + +-
        Updated dependencies [cd0d50016e, 79c16d315a]: + + - @rocket.chat/gazzodown@11.0.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 +
        + ## 1.19.3 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index ce7bb92d6b78..ab4a2a9b3a11 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.19.3", + "version": "1.20.0-rc.0", "files": [ "/build" ], diff --git a/packages/message-parser/CHANGELOG.md b/packages/message-parser/CHANGELOG.md index 39c82e350b58..f263b04e632c 100644 --- a/packages/message-parser/CHANGELOG.md +++ b/packages/message-parser/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.31.30-rc.0 + +### Patch Changes + +- ([#33227](https://github.com/RocketChat/Rocket.Chat/pull/33227)) Improved the performance of the message parser + ## 0.31.29 ### Patch Changes diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index e58727ba38c2..39fbb4bda5dd 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/message-parser", "description": "Rocket.Chat parser for messages", - "version": "0.31.29", + "version": "0.31.30-rc.0", "author": { "name": "Rocket.Chat", "url": "https://rocket.chat/" diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 3a2edfdf99ff..5bd539fda340 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.1.3-rc.0 + +### Patch Changes + +-
        Updated dependencies [bb94c9c67a, 7c14fd1a80, 274f4f5881, 0f21fa01a3]: + + - @rocket.chat/i18n@0.8.0-rc.0 +
        + ## 0.1.2 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index f9e9b4d06ab8..1a7051e87ff2 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.2", + "version": "0.1.3-rc.0", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index b743a1132b48..f0aa631439e9 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,24 @@ # @rocket.chat/model-typings +## 0.8.0-rc.0 + +### Minor Changes + +- ([#32682](https://github.com/RocketChat/Rocket.Chat/pull/32682)) Added support for specifying a unit on departments' creation and update + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +- ([#33177](https://github.com/RocketChat/Rocket.Chat/pull/33177)) New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 +
        + ## 0.7.0 ### Minor Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 30144dabb49a..10a2fd9b643c 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.7.0", + "version": "0.8.0-rc.0", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 1238c213ec4f..1251d75001bb 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/models +## 0.3.0-rc.0 + +### Minor Changes + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +### Patch Changes + +-
        Updated dependencies [9a38c8e13f, 927710d778, 3a161c4310, 12d6307998]: + + - @rocket.chat/model-typings@0.8.0-rc.0 +
        + ## 0.2.3 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index e55ce8d5a0e1..35064171d9a6 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.2.3", + "version": "0.3.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/peggy-loader/CHANGELOG.md b/packages/peggy-loader/CHANGELOG.md index 0c1ace99d7d4..422c09e4bc9a 100644 --- a/packages/peggy-loader/CHANGELOG.md +++ b/packages/peggy-loader/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## 0.31.26-rc.0 + +### Patch Changes + +- ([#33227](https://github.com/RocketChat/Rocket.Chat/pull/33227)) Improved the performance of the message parser + All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index 6eb584f62d24..464be6fdc297 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/peggy-loader", - "version": "0.31.25", + "version": "0.31.26-rc.0", "description": "Peggy loader for webpack", "keywords": [ "peggy", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 7cce8dcc6e99..b11458549219 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,29 @@ # @rocket.chat/rest-typings +## 6.13.0-rc.0 + +### Minor Changes + +- ([#32682](https://github.com/RocketChat/Rocket.Chat/pull/32682)) Added support for specifying a unit on departments' creation and update + +- ([#32729](https://github.com/RocketChat/Rocket.Chat/pull/32729)) Implemented "omnichannel/contacts.update" endpoint to update contacts + +- ([#33011](https://github.com/RocketChat/Rocket.Chat/pull/33011)) Return `parent` and `team` information when calling `rooms.info` endpoint + +- ([#32693](https://github.com/RocketChat/Rocket.Chat/pull/32693)) Introduced "create contacts" endpoint to omnichannel + +- ([#33177](https://github.com/RocketChat/Rocket.Chat/pull/33177)) New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [274f4f5881, 79c16d315a, 927710d778, 12d6307998]: + + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/message-parser@0.31.30-rc.0 +
        + ## 6.12.0 ### Minor Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 972577210f56..33aad6d64823 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-develop", + "version": "6.13.0-rc.0", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index a9fa21cf7195..cfb5f9e17d20 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/ui-avatar +## 7.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 6.0.0 ### Patch Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 0423b68f2b43..a32a7a8be6bf 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "6.0.0", + "version": "7.0.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 1d172fe1054f..5471cf97ea1b 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,23 @@ # @rocket.chat/ui-client +## 11.0.0-rc.0 + +### Minor Changes + +- ([#33156](https://github.com/RocketChat/Rocket.Chat/pull/33156)) added `sidepanelNavigation` to feature preview list + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +- ([#33225](https://github.com/RocketChat/Rocket.Chat/pull/33225)) Implemented new feature preview for Sidepanel + +### Patch Changes + +-
        Updated dependencies [cd0d50016e]: + + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index a84317f37abc..82705ba026cb 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -61,8 +61,8 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-avatar": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-avatar": "7.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "react": "*", "react-i18next": "*" }, diff --git a/packages/ui-composer/CHANGELOG.md b/packages/ui-composer/CHANGELOG.md index 757dee9a2549..20d5d6ac715d 100644 --- a/packages/ui-composer/CHANGELOG.md +++ b/packages/ui-composer/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/ui-composer +## 0.3.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + ## 0.2.1 ### Patch Changes diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 0f97fef22508..363299698bfa 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-composer", - "version": "0.2.1", + "version": "0.3.0-rc.0", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index aa0f00bc0e83..21aeff09a896 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/ui-contexts +## 11.0.0-rc.0 + +### Patch Changes + +-
        Updated dependencies [bb94c9c67a, 9a38c8e13f, 7c14fd1a80, 9eaefdc892, 274f4f5881, 532f08819e, 927710d778, 3a161c4310, 0f21fa01a3, 12d6307998]: + + - @rocket.chat/i18n@0.8.0-rc.0 + - @rocket.chat/rest-typings@6.13.0-rc.0 + - @rocket.chat/core-typings@6.13.0-rc.0 + - @rocket.chat/ddp-client@0.3.7-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 3ef588432a7c..37f83e96a2aa 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index 0ebba8028576..fe47f06728bd 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ui-video-conf +## 11.0.0-rc.0 + +### Minor Changes + +- ([#32821](https://github.com/RocketChat/Rocket.Chat/pull/32821)) Replaced new `SidebarV2` components under feature preview + +### Patch Changes + +-
        Updated dependencies [cd0d50016e]: + + - @rocket.chat/ui-avatar@7.0.0-rc.0 + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index eb363da1ceae..0f1f3e50eab8 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -39,8 +39,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "*", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-avatar": "7.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index 601f9aa63494..0a222203fdee 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 11.0.0-rc.0 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.0 +
        + ## 10.0.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index ae08e05ee218..23fd80c89842 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "10.0.0", + "version": "11.0.0-rc.0", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "*", + "@rocket.chat/ui-contexts": "11.0.0-rc.0", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", diff --git a/yarn.lock b/yarn.lock index 6b1123701996..c0fb2733310e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10493,6 +10493,7 @@ __metadata: "@codemirror/lang-json": ^6.0.1 "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.59.0 "@rocket.chat/fuselage-hooks": ^0.33.1 From 65d2a453f0f0a5236d710ae0ef0587e6aabcb19d Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 20 Sep 2024 10:56:48 -0300 Subject: [PATCH 35/57] chore: update E2EE setting text (#33226) --- packages/i18n/src/locales/en.i18n.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8ce6bea2e117..9f37642263da 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1788,8 +1788,8 @@ "Duplicated_Email_address_will_be_ignored": "Duplicated email address will be ignored.", "Markdown_Marked_Tables": "Enable Marked Tables", "duplicated-account": "Duplicated account", - "E2E_Allow_Unencrypted_Messages": "Unencrypted messages in encrypted rooms", - "E2E_Allow_Unencrypted_Messages_Description": "Allow plain text messages to be sent in encrypted rooms. These messages will not be encrypted.", + "E2E_Allow_Unencrypted_Messages": "Access unencrypted content in encrypted rooms", + "E2E_Allow_Unencrypted_Messages_Description": "Allow access to encrypted rooms to people without room encryption keys. They'll be able to see and send unencrypted messages.", "E2E Encryption": "E2E Encryption", "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}", From 9bcb802fdcd2a458ae8f8cd69d94173b4f40861d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Guimar=C3=A3es=20Ribeiro?= <43561537+rique223@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:58:34 -0300 Subject: [PATCH 36/57] feat: Implement proper accessbility for report user modal (#33294) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/late-hats-carry.md | 6 ++++ .../UserInfo/ReportUserModal.tsx | 31 +++++++++++-------- packages/i18n/src/locales/en.i18n.json | 2 ++ 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 .changeset/late-hats-carry.md diff --git a/.changeset/late-hats-carry.md b/.changeset/late-hats-carry.md new file mode 100644 index 000000000000..ec24c7cd5376 --- /dev/null +++ b/.changeset/late-hats-carry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +--- + +Improves the accessibility of the report user modal by adding an appropriate label, description, and ARIA attributes. diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx index 5f94f7c407b0..86b4571d88d1 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/ReportUserModal.tsx @@ -1,4 +1,4 @@ -import { Box, FieldGroup, Field, FieldLabel, FieldRow, FieldError, TextAreaInput } from '@rocket.chat/fuselage'; +import { Box, FieldGroup, Field, FieldLabel, FieldRow, FieldError, TextAreaInput, FieldDescription } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { UserAvatar } from '@rocket.chat/ui-avatar'; import type { ComponentProps } from 'react'; @@ -45,27 +45,32 @@ const ReportUserModal = ({ username, displayName, onConfirm, onClose }: ReportUs onCancel={onClose} confirmText={t('Report')} > + + + + {displayName} + + - - - - - {displayName} - - - + {t('Report_reason')} + {t('Let_moderators_know_what_the_issue_is')} - {errors.reasonForReport && {errors.reasonForReport.message}} + {errors.reasonForReport && ( + + {errors.reasonForReport.message} + + )} diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 9f37642263da..0e99c1bdc1d8 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -3199,6 +3199,7 @@ "leave-p": "Leave Private Groups", "leave-p_description": "Permission to leave private groups", "Lets_get_you_new_one_": "Let's get you a new one!", + "Let_moderators_know_what_the_issue_is": "Let moderators know what the issue is", "Let_them_know": "Let them know", "Left": "Left", "License": "License", @@ -4490,6 +4491,7 @@ "Report_exclamation_mark": "Report!", "Report_has_been_sent": "Report has been sent", "Report_Number": "Report Number", + "Report_reason": "Report reason", "Report_this_message_question_mark": "Report this message?", "Report_User": "Report user", "Reporting": "Reporting", From 827850d545043896722b53d2ed81af3e4d0738b7 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Mon, 23 Sep 2024 21:36:58 +0200 Subject: [PATCH 37/57] fix: imported fixes (#33330) --- .changeset/little-bottles-peel.md | 5 +++ apps/meteor/app/api/server/v1/rooms.ts | 9 +++- .../lib/server/methods/cleanRoomHistory.ts | 8 ++++ .../content/urlPreviews/OEmbedHtmlPreview.tsx | 11 ++++- apps/meteor/tests/end-to-end/api/methods.ts | 43 ++++++++++++++++++- apps/meteor/tests/end-to-end/api/rooms.ts | 28 ++++++++++++ 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .changeset/little-bottles-peel.md diff --git a/.changeset/little-bottles-peel.md b/.changeset/little-bottles-peel.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/little-bottles-peel.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 3dc62e462ddf..117ae3851c43 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -40,7 +40,7 @@ import { findRoomsAvailableForTeams, } from '../lib/rooms'; -async function findRoomByIdOrName({ +export async function findRoomByIdOrName({ params, checkedArchived = true, }: { @@ -365,7 +365,12 @@ API.v1.addRoute( { authRequired: true, validateParams: isRoomsCleanHistoryProps }, { async post() { - const { _id } = await findRoomByIdOrName({ params: this.bodyParams }); + const room = await findRoomByIdOrName({ params: this.bodyParams }); + const { _id } = room; + + if (!room || !(await canAccessRoomAsync(room, { _id: this.userId }))) { + return API.v1.failure('User does not have access to the room [error-not-allowed]', 'error-not-allowed'); + } const { latest, diff --git a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts index d6136eee9131..c804128d27bd 100644 --- a/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/methods/cleanRoomHistory.ts @@ -2,6 +2,8 @@ import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; +import { findRoomByIdOrName } from '../../../api/server/v1/rooms'; +import { canAccessRoomAsync } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { cleanRoomHistory } from '../functions/cleanRoomHistory'; @@ -56,6 +58,12 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); } + const room = await findRoomByIdOrName({ params: { roomId } }); + + if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'cleanRoomHistory' }); + } + return cleanRoomHistory({ rid: roomId, latest, diff --git a/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx b/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx index e8dd4e1ddcc2..518f1ebad203 100644 --- a/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx +++ b/apps/meteor/client/components/message/content/urlPreviews/OEmbedHtmlPreview.tsx @@ -1,12 +1,21 @@ import { Box } from '@rocket.chat/fuselage'; +import DOMPurify from 'dompurify'; import type { ReactElement } from 'react'; import React from 'react'; import OEmbedCollapsible from './OEmbedCollapsible'; import type { OEmbedPreviewMetadata } from './OEmbedPreviewMetadata'; +const purifyOptions = { + ADD_TAGS: ['iframe'], + ADD_ATTR: ['frameborder', 'allow', 'allowfullscreen', 'scrolling', 'src', 'style', 'referrerpolicy'], + ALLOW_UNKNOWN_PROTOCOLS: true, +}; + const OEmbedHtmlPreview = ({ html, ...props }: OEmbedPreviewMetadata): ReactElement => ( - {html && } + + {html && } + ); export default OEmbedHtmlPreview; diff --git a/apps/meteor/tests/end-to-end/api/methods.ts b/apps/meteor/tests/end-to-end/api/methods.ts index 08945994e438..e3c42389e506 100644 --- a/apps/meteor/tests/end-to-end/api/methods.ts +++ b/apps/meteor/tests/end-to-end/api/methods.ts @@ -616,9 +616,19 @@ describe('Meteor.methods', () => { describe('[@cleanRoomHistory]', () => { let rid: IRoom['_id']; - + let testUser: IUser; + let testUserCredentials: Credentials; let channelName: string; + before('update permissions', async () => { + await updatePermission('clean-channel-history', ['admin', 'user']); + }); + + before('create test user', async () => { + testUser = await createUser(); + testUserCredentials = await login(testUser.username, password); + }); + before('create room', (done) => { channelName = `methods-test-channel-${Date.now()}`; void request @@ -676,7 +686,36 @@ describe('Meteor.methods', () => { .end(done); }); - after(() => deleteRoom({ type: 'p', roomId: rid })); + after(() => + Promise.all([deleteRoom({ type: 'p', roomId: rid }), deleteUser(testUser), updatePermission('clean-channel-history', ['admin'])]), + ); + + it('should throw an error if user is not part of the room', async () => { + await request + .post(methodCall('cleanRoomHistory')) + .set(testUserCredentials) + .send({ + message: JSON.stringify({ + method: 'cleanRoomHistory', + params: [ + { + roomId: rid, + oldest: { $date: new Date().getTime() }, + latest: { $date: new Date().getTime() }, + }, + ], + id: 'id', + msg: 'method', + }), + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.a.property('success', true); + const data = JSON.parse(res.body.message); + expect(data).to.have.a.property('error').that.is.an('object'); + expect(data.error).to.have.a.property('error', 'error-not-allowed'); + }); + }); it('should not change the _updatedAt value when nothing is changed on the room', async () => { const roomBefore = await request.get(api('groups.info')).set(credentials).query({ diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index 15f85964ffff..5047af7956d8 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -1133,6 +1133,34 @@ describe('[Rooms]', () => { }) .end(done); }); + describe('test user is not part of room', async () => { + beforeEach(async () => { + await updatePermission('clean-channel-history', ['admin', 'user']); + }); + + afterEach(async () => { + await updatePermission('clean-channel-history', ['admin']); + }); + + it('should return an error when the user with right privileges is not part of the room', async () => { + await request + .post(api('rooms.cleanHistory')) + .set(userCredentials) + .send({ + roomId: privateChannel._id, + latest: '9999-12-31T23:59:59.000Z', + oldest: '0001-01-01T00:00:00.000Z', + limit: 2000, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('errorType', 'error-not-allowed'); + expect(res.body).to.have.property('error', 'User does not have access to the room [error-not-allowed]'); + }); + }); + }); }); describe('[/rooms.info]', () => { let testChannel: IRoom; From a6b91525a8c28a640a6e378a912abc06c4ef6bae Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Mon, 23 Sep 2024 19:00:02 -0300 Subject: [PATCH 38/57] chore: create network broker package (#33338) --- _templates/service/new/package.json.ejs.t | 1 + _templates/service/new/service.ejs.t | 4 +- .../ee/server/services/ecdh-proxy/service.ts | 3 +- apps/meteor/ee/server/services/package.json | 1 + apps/meteor/ee/server/startup/index.ts | 2 +- apps/meteor/package.json | 1 + ee/apps/account-service/Dockerfile | 3 ++ ee/apps/account-service/package.json | 1 + ee/apps/account-service/src/service.ts | 2 +- ee/apps/authorization-service/Dockerfile | 3 ++ ee/apps/authorization-service/package.json | 1 + ee/apps/authorization-service/src/service.ts | 2 +- ee/apps/ddp-streamer/Dockerfile | 3 ++ ee/apps/ddp-streamer/package.json | 1 + ee/apps/ddp-streamer/src/service.ts | 2 +- ee/apps/omnichannel-transcript/Dockerfile | 3 ++ ee/apps/omnichannel-transcript/package.json | 1 + ee/apps/omnichannel-transcript/src/service.ts | 2 +- ee/apps/presence-service/Dockerfile | 3 ++ ee/apps/presence-service/package.json | 1 + ee/apps/presence-service/src/service.ts | 2 +- ee/apps/queue-worker/Dockerfile | 3 ++ ee/apps/queue-worker/package.json | 1 + ee/apps/queue-worker/src/service.ts | 2 +- ee/apps/stream-hub-service/Dockerfile | 3 ++ ee/apps/stream-hub-service/package.json | 1 + ee/apps/stream-hub-service/src/service.ts | 2 +- ee/packages/network-broker/.eslintrc.json | 4 ++ ee/packages/network-broker/jest.config.ts | 6 +++ ee/packages/network-broker/package.json | 39 +++++++++++++++++++ .../network-broker/src}/EnterpriseCheck.ts | 0 .../network-broker/src/NetworkBroker.test.ts | 4 +- .../network-broker/src}/NetworkBroker.ts | 16 +++++--- .../packages/network-broker/src/index.ts | 2 +- ee/packages/network-broker/tsconfig.json | 9 +++++ yarn.lock | 31 +++++++++++++++ 36 files changed, 145 insertions(+), 20 deletions(-) create mode 100644 ee/packages/network-broker/.eslintrc.json create mode 100644 ee/packages/network-broker/jest.config.ts create mode 100644 ee/packages/network-broker/package.json rename {apps/meteor/ee/server/lib => ee/packages/network-broker/src}/EnterpriseCheck.ts (100%) rename apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts => ee/packages/network-broker/src/NetworkBroker.test.ts (85%) rename {apps/meteor/ee/server => ee/packages/network-broker/src}/NetworkBroker.ts (94%) rename apps/meteor/ee/server/startup/broker.ts => ee/packages/network-broker/src/index.ts (98%) create mode 100644 ee/packages/network-broker/tsconfig.json diff --git a/_templates/service/new/package.json.ejs.t b/_templates/service/new/package.json.ejs.t index 2c74278d1ced..0aa1bd69e995 100644 --- a/_templates/service/new/package.json.ejs.t +++ b/_templates/service/new/package.json.ejs.t @@ -20,6 +20,7 @@ to: ee/apps/<%= name %>/package.json "dependencies": { "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/emitter": "next", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 54080d94cf08..77f02d2b7769 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -1,11 +1,11 @@ --- to: ee/apps/<%= name %>/src/service.ts --- +import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; -import { api } from '@rocket.chat/core-services'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; diff --git a/apps/meteor/ee/server/services/ecdh-proxy/service.ts b/apps/meteor/ee/server/services/ecdh-proxy/service.ts index 7ef3e8d26dcc..a795f157c9a5 100755 --- a/apps/meteor/ee/server/services/ecdh-proxy/service.ts +++ b/apps/meteor/ee/server/services/ecdh-proxy/service.ts @@ -1,5 +1,4 @@ -import '../../startup/broker'; - +import '@rocket.chat/network-broker'; import { api } from '@rocket.chat/core-services'; import { ECDHProxy } from './ECDHProxy'; diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 43659382eb67..a2adb5872ad2 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -25,6 +25,7 @@ "@rocket.chat/message-parser": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/ui-kit": "workspace:~", diff --git a/apps/meteor/ee/server/startup/index.ts b/apps/meteor/ee/server/startup/index.ts index a8091f0e9a37..eb09ca6ed30f 100644 --- a/apps/meteor/ee/server/startup/index.ts +++ b/apps/meteor/ee/server/startup/index.ts @@ -13,7 +13,7 @@ import { isRunningMs } from '../../../server/lib/isRunningMs'; export const registerEEBroker = async (): Promise => { // only starts network broker if running in micro services mode if (isRunningMs()) { - const { broker } = await import('./broker'); + const { broker } = await import('@rocket.chat/network-broker'); api.setBroker(broker); void api.start(); diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 9ee7e48d1794..c7da95059475 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -262,6 +262,7 @@ "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/onboarding-ui": "~0.33.3", "@rocket.chat/password-policies": "workspace:^", diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index acbc5b0371d2..c80d4f2eb376 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index b9e45ed14eda..6f92b8eb9078 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@rocket.chat/tools": "workspace:^", diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts index f166233ca137..07ca30ed748f 100755 --- a/ee/apps/account-service/src/service.ts +++ b/ee/apps/account-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3033; diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index c662d8765300..9a9e8ded922c 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index b7912b8bc94d..262fb6789f9d 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts index 29162b636229..4dcd466afa60 100755 --- a/ee/apps/authorization-service/src/service.ts +++ b/ee/apps/authorization-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3034; diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index f556cbde6752..32103dc3528b 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -40,6 +40,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 87029e4b8993..84859add8d7b 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -23,6 +23,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "colorette": "^1.4.0", diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index b5cd20f7ec02..07666a265dbe 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,9 +1,9 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; (async () => { const db = await getConnection(); diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 6a93a8e5e8be..9b7e47968e68 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 27290070b653..ced114bfb589 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -22,6 +22,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/tools": "workspace:^", diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts index 14cbc5b8438a..66456456fb74 100644 --- a/ee/apps/omnichannel-transcript/src/service.ts +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3036; diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index aa9c1c0bd6c9..430880d29606 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -40,6 +40,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 5968631341b0..d8e47cc5c5ea 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -20,6 +20,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index b7275a29106f..0e1c97f2daa2 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,10 +1,10 @@ import { api } from '@rocket.chat/core-services'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3031; diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 6a93a8e5e8be..9b7e47968e68 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b3d8b0aff94e..4270818a6eb3 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -22,6 +22,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/omnichannel-services": "workspace:^", "@types/node": "^14.18.63", "ejson": "^2.2.3", diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts index 8583257a6860..4bc6c9642913 100644 --- a/ee/apps/queue-worker/src/service.ts +++ b/ee/apps/queue-worker/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; const PORT = process.env.PORT || 3038; diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index c662d8765300..9a9e8ded922c 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -37,6 +37,9 @@ COPY ./packages/logger/dist packages/logger/dist COPY ./packages/server-cloud-communication/ packages/server-cloud-communication/ +COPY ./ee/packages/network-broker/package.json ee/packages/network-broker/package.json +COPY ./ee/packages/network-broker/dist ee/packages/network-broker/dist + COPY ./ee/packages/license/package.json packages/license/package.json COPY ./ee/packages/license/dist packages/license/dist diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 9fd5cf0586f6..66443ebeec68 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -21,6 +21,7 @@ "@rocket.chat/logger": "workspace:^", "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", + "@rocket.chat/network-broker": "workspace:^", "@rocket.chat/string-helpers": "~0.31.25", "@types/node": "^14.18.63", "ejson": "^2.2.3", diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts index 4975b5b306be..eade703321d2 100755 --- a/ee/apps/stream-hub-service/src/service.ts +++ b/ee/apps/stream-hub-service/src/service.ts @@ -1,11 +1,11 @@ import { api } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; +import { broker } from '@rocket.chat/network-broker'; import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; -import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; import { StreamHub } from './StreamHub'; diff --git a/ee/packages/network-broker/.eslintrc.json b/ee/packages/network-broker/.eslintrc.json new file mode 100644 index 000000000000..a83aeda48e66 --- /dev/null +++ b/ee/packages/network-broker/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/network-broker/jest.config.ts b/ee/packages/network-broker/jest.config.ts new file mode 100644 index 000000000000..c18c8ae02465 --- /dev/null +++ b/ee/packages/network-broker/jest.config.ts @@ -0,0 +1,6 @@ +import server from '@rocket.chat/jest-presets/server'; +import type { Config } from 'jest'; + +export default { + preset: server.preset, +} satisfies Config; diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json new file mode 100644 index 000000000000..c4d3ea1b284b --- /dev/null +++ b/ee/packages/network-broker/package.json @@ -0,0 +1,39 @@ +{ + "name": "@rocket.chat/network-broker", + "version": "0.1.0", + "private": true, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/chai": "~4.3.19", + "@types/ejson": "^2.2.2", + "@types/node": "^14.18.63", + "@types/sinon": "^10.0.20", + "chai": "^4.3.10", + "eslint": "~8.45.0", + "jest": "~29.7.0", + "sinon": "^14.0.2", + "typescript": "~5.5.4" + }, + "scripts": { + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "test": "jest", + "build": "tsc", + "testunit": "jest", + "typecheck": "tsc --noEmit --skipLibCheck" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + }, + "dependencies": { + "@rocket.chat/core-services": "workspace:^", + "ejson": "^2.2.3", + "moleculer": "^0.14.34", + "pino": "^8.15.0" + } +} diff --git a/apps/meteor/ee/server/lib/EnterpriseCheck.ts b/ee/packages/network-broker/src/EnterpriseCheck.ts similarity index 100% rename from apps/meteor/ee/server/lib/EnterpriseCheck.ts rename to ee/packages/network-broker/src/EnterpriseCheck.ts diff --git a/apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts b/ee/packages/network-broker/src/NetworkBroker.test.ts similarity index 85% rename from apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts rename to ee/packages/network-broker/src/NetworkBroker.test.ts index 1aac9c33fc33..c79fb12e7049 100644 --- a/apps/meteor/ee/tests/unit/server/NetworkBroker.tests.ts +++ b/ee/packages/network-broker/src/NetworkBroker.test.ts @@ -2,8 +2,8 @@ import { ServiceClass } from '@rocket.chat/core-services'; import { expect } from 'chai'; import sinon from 'sinon'; -import { BrokerMocked } from '../../../../tests/mocks/server/BrokerMocked'; -import { NetworkBroker } from '../../../server/NetworkBroker'; +import { BrokerMocked } from '../../../../apps/meteor/tests/mocks/server/BrokerMocked'; +import { NetworkBroker } from './NetworkBroker'; class DelayedStopBroker extends BrokerMocked { async destroyService(name: string) { diff --git a/apps/meteor/ee/server/NetworkBroker.ts b/ee/packages/network-broker/src/NetworkBroker.ts similarity index 94% rename from apps/meteor/ee/server/NetworkBroker.ts rename to ee/packages/network-broker/src/NetworkBroker.ts index 0fed6fca542d..e326357cba0a 100644 --- a/apps/meteor/ee/server/NetworkBroker.ts +++ b/ee/packages/network-broker/src/NetworkBroker.ts @@ -2,7 +2,7 @@ import { asyncLocalStorage } from '@rocket.chat/core-services'; import type { IBroker, IBrokerNode, IServiceMetrics, IServiceClass, EventSignatures } from '@rocket.chat/core-services'; import type { ServiceBroker, Context, ServiceSchema } from 'moleculer'; -import { EnterpriseCheck } from './lib/EnterpriseCheck'; +import { EnterpriseCheck } from './EnterpriseCheck'; const events: { [k: string]: string } = { onNodeConnected: '$node.connected', @@ -25,7 +25,7 @@ const waitForServicesTimeout = parseInt(WAIT_FOR_SERVICES_TIMEOUT, 10) || 10000; export class NetworkBroker implements IBroker { private broker: ServiceBroker; - private started: Promise; + private started: Promise = Promise.resolve(false); metrics: IServiceMetrics; @@ -36,7 +36,9 @@ export class NetworkBroker implements IBroker { } async call(method: string, data: any): Promise { - await this.started; + if (!(await this.started)) { + return; + } const context = asyncLocalStorage.getStore(); @@ -54,7 +56,9 @@ export class NetworkBroker implements IBroker { } async waitAndCall(method: string, data: any): Promise { - await this.started; + if (!(await this.started)) { + return; + } try { await this.broker.waitForServices(method.split('.')[0], waitForServicesTimeout); @@ -182,6 +186,8 @@ export class NetworkBroker implements IBroker { } async start(): Promise { - this.started = this.broker.start(); + await this.broker.start(); + + this.started = Promise.resolve(true); } } diff --git a/apps/meteor/ee/server/startup/broker.ts b/ee/packages/network-broker/src/index.ts similarity index 98% rename from apps/meteor/ee/server/startup/broker.ts rename to ee/packages/network-broker/src/index.ts index daae4ace4e05..caa12890b514 100644 --- a/apps/meteor/ee/server/startup/broker.ts +++ b/ee/packages/network-broker/src/index.ts @@ -3,7 +3,7 @@ import EJSON from 'ejson'; import { Errors, Serializers, ServiceBroker } from 'moleculer'; import { pino } from 'pino'; -import { NetworkBroker } from '../NetworkBroker'; +import { NetworkBroker } from './NetworkBroker'; const { MS_NAMESPACE = '', diff --git a/ee/packages/network-broker/tsconfig.json b/ee/packages/network-broker/tsconfig.json new file mode 100644 index 000000000000..ada83b80ff89 --- /dev/null +++ b/ee/packages/network-broker/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + }, + "files": ["./src/index.ts"] +} diff --git a/yarn.lock b/yarn.lock index 6b1123701996..a7d363694243 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8441,6 +8441,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/tools": "workspace:^" @@ -8556,6 +8557,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/gc-stats": ^1.4.3 @@ -8719,6 +8721,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/ejson": ^2.2.2 @@ -9396,6 +9399,7 @@ __metadata: "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/mp3-encoder": 0.24.0 + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/onboarding-ui": ~0.33.3 "@rocket.chat/password-policies": "workspace:^" @@ -9771,6 +9775,27 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/network-broker@workspace:^, @rocket.chat/network-broker@workspace:ee/packages/network-broker": + version: 0.0.0-use.local + resolution: "@rocket.chat/network-broker@workspace:ee/packages/network-broker" + dependencies: + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/eslint-config": "workspace:^" + "@types/chai": ~4.3.19 + "@types/ejson": ^2.2.2 + "@types/node": ^14.18.63 + "@types/sinon": ^10.0.20 + chai: ^4.3.10 + ejson: ^2.2.3 + eslint: ~8.45.0 + jest: ~29.7.0 + moleculer: ^0.14.34 + pino: ^8.15.0 + sinon: ^14.0.2 + typescript: ~5.5.4 + languageName: unknown + linkType: soft + "@rocket.chat/omnichannel-services@workspace:^, @rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services": version: 0.0.0-use.local resolution: "@rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services" @@ -9817,6 +9842,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/pdf-worker": "workspace:^" "@rocket.chat/tools": "workspace:^" @@ -9954,6 +9980,7 @@ __metadata: "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/gc-stats": ^1.4.3 @@ -10019,6 +10046,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/omnichannel-services": "workspace:^" "@types/gc-stats": ^1.4.3 "@types/node": ^14.18.63 @@ -10176,6 +10204,7 @@ __metadata: "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@types/bcrypt": ^5.0.2 @@ -10493,6 +10522,7 @@ __metadata: "@codemirror/lang-json": ^6.0.1 "@codemirror/tooltip": ^0.19.16 "@lezer/highlight": ^1.1.6 + "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/fuselage": ^0.59.0 "@rocket.chat/fuselage-hooks": ^0.33.1 @@ -37207,6 +37237,7 @@ __metadata: "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" + "@rocket.chat/network-broker": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": ~0.31.25 "@rocket.chat/ui-kit": "workspace:~" From 1f89f780bd205d36b572689e1103c860f787a13e Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Tue, 24 Sep 2024 09:54:31 -0300 Subject: [PATCH 39/57] feat: Adds new admin feature preview setting management (#33212) Co-authored-by: Guilherme Gazzo --- .changeset/quick-rings-wave.md | 7 + .../UserMenu/hooks/useAccountItems.tsx | 4 +- .../hooks/useFeaturePreviewEnableQuery.ts | 28 ++++ .../sidebar/header/hooks/useAccountItems.tsx | 4 +- .../AccountFeaturePreviewBadge.tsx | 21 --- .../AccountFeaturePreviewPage.tsx | 44 ++---- .../client/views/account/sidebarItems.tsx | 5 +- .../AdminFeaturePreviewPage.tsx | 127 ++++++++++++++++++ .../AdminFeaturePreviewRoute.tsx | 26 ++++ apps/meteor/client/views/admin/routes.tsx | 9 ++ .../meteor/client/views/admin/sidebarItems.ts | 8 ++ .../featurePreview/enhanced-navigation.png | Bin 0 -> 2372 bytes .../resizable-contextual-bar.png | Bin 0 -> 4776 bytes .../images/featurePreview/timestamp.png | Bin 0 -> 51432 bytes apps/meteor/server/settings/accounts.ts | 5 + .../tests/end-to-end/api/miscellaneous.ts | 1 + packages/i18n/src/locales/en.i18n.json | 16 ++- packages/i18n/src/locales/hi-IN.i18n.json | 6 +- .../FeaturePreview/FeaturePreviewBadge.tsx | 21 +++ .../src/components/FeaturePreview/index.ts | 2 + packages/ui-client/src/components/index.ts | 2 +- .../useDefaultSettingFeaturePreviewList.ts | 12 ++ .../ui-client/src/hooks/useFeaturePreview.ts | 5 +- .../src/hooks/useFeaturePreviewList.ts | 32 +++-- ... usePreferenceFeaturePreviewList.spec.tsx} | 13 +- .../hooks/usePreferenceFeaturePreviewList.ts | 16 +++ packages/ui-client/src/index.ts | 2 + 27 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 .changeset/quick-rings-wave.md create mode 100644 apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts delete mode 100644 apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx create mode 100644 apps/meteor/public/images/featurePreview/enhanced-navigation.png create mode 100644 apps/meteor/public/images/featurePreview/resizable-contextual-bar.png create mode 100644 apps/meteor/public/images/featurePreview/timestamp.png create mode 100644 packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx create mode 100644 packages/ui-client/src/components/FeaturePreview/index.ts create mode 100644 packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts rename packages/ui-client/src/hooks/{useFeaturePreviewList.spec.tsx => usePreferenceFeaturePreviewList.spec.tsx} (79%) create mode 100644 packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts diff --git a/.changeset/quick-rings-wave.md b/.changeset/quick-rings-wave.md new file mode 100644 index 000000000000..0ea22897ff45 --- /dev/null +++ b/.changeset/quick-rings-wave.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/ui-client": minor +--- + +Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx index 82c39c5c1b10..e54e2b72d675 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useEffectEvent(() => { router.navigate('/account'); diff --git a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts new file mode 100644 index 000000000000..fd88f0237d29 --- /dev/null +++ b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts @@ -0,0 +1,28 @@ +import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; +import { useMemo } from 'react'; + +const handleFeaturePreviewEnableQuery = (item: FeaturePreviewProps, _: any, features: FeaturePreviewProps[]) => { + if (item.enableQuery) { + const expected = item.enableQuery.value; + const received = features.find((el) => el.name === item.enableQuery?.name)?.value; + if (expected !== received) { + item.disabled = true; + item.value = false; + } else { + item.disabled = false; + } + } + return item; +}; + +const groupFeaturePreview = (features: FeaturePreviewProps[]) => + Object.entries( + features.reduce((result, currentValue) => { + (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); + return result; + }, {} as Record), + ); + +export const useFeaturePreviewEnableQuery = (features: FeaturePreviewProps[]) => { + return useMemo(() => groupFeaturePreview(features.map(handleFeaturePreviewEnableQuery)), [features]); +}; diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index 2be6b2b1dea2..51ab7a198a67 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useMutableCallback(() => { router.navigate('/account'); diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx deleted file mode 100644 index c109ca1aefb5..000000000000 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Badge } from '@rocket.chat/fuselage'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -const AccountFeaturePreviewBadge = () => { - const { t } = useTranslation(); - const { unseenFeatures } = useFeaturePreviewList(); - - if (!unseenFeatures) { - return null; - } - - return ( - - {unseenFeatures} - - ); -}; - -export default AccountFeaturePreviewBadge; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index dd9ab6a90959..358d2394003b 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { ButtonGroup, Button, @@ -13,9 +12,10 @@ import { FieldLabel, FieldRow, FieldHint, + Callout, + Margins, } from '@rocket.chat/fuselage'; -import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; +import { usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; @@ -23,26 +23,12 @@ import React, { useEffect, Fragment } from 'react'; import { useForm } from 'react-hook-form'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; -const handleEnableQuery = (features: FeaturePreviewProps[]) => { - return features.map((item) => { - if (item.enableQuery) { - const expected = item.enableQuery.value; - const received = features.find((el) => el.name === item.enableQuery?.name)?.value; - if (expected !== received) { - item.disabled = true; - item.value = false; - } else { - item.disabled = false; - } - } - return item; - }); -}; const AccountFeaturePreviewPage = () => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { features, unseenFeatures } = useFeaturePreviewList(); + const { features, unseenFeatures } = usePreferenceFeaturePreviewList(); const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); @@ -85,12 +71,7 @@ const AccountFeaturePreviewPage = () => { setValue('featuresPreview', updated, { shouldDirty: true }); }; - const grouppedFeaturesPreview = Object.entries( - handleEnableQuery(featuresPreview).reduce((result, currentValue) => { - (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); - return result; - }, {} as Record), - ); + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); return ( @@ -105,14 +86,11 @@ const AccountFeaturePreviewPage = () => { )} {featuresPreview.length > 0 && ( <> - - {t('Feature_preview_page_description')} + + + {t('Feature_preview_page_description')} + {t('Feature_preview_page_callout')} + {grouppedFeaturesPreview?.map(([group, features], index) => ( diff --git a/apps/meteor/client/views/account/sidebarItems.tsx b/apps/meteor/client/views/account/sidebarItems.tsx index ca0376be329d..fa2ab8bd5e40 100644 --- a/apps/meteor/client/views/account/sidebarItems.tsx +++ b/apps/meteor/client/views/account/sidebarItems.tsx @@ -1,10 +1,9 @@ -import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, FeaturePreviewBadge } from '@rocket.chat/ui-client'; import React from 'react'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client'; import { settings } from '../../../app/settings/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; -import AccountFeaturePreviewBadge from './featurePreview/AccountFeaturePreviewBadge'; export const { registerSidebarItem: registerAccountSidebarItem, @@ -54,7 +53,7 @@ export const { href: '/account/feature-preview', i18nLabel: 'Feature_preview', icon: 'flask', - badge: () => , + badge: () => , permissionGranted: () => settings.get('Accounts_AllowFeaturePreview') && defaultFeaturesPreview?.length > 0, }, { diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx new file mode 100644 index 000000000000..615fd20cf5a6 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx @@ -0,0 +1,127 @@ +import { + ButtonGroup, + Button, + Box, + ToggleSwitch, + Accordion, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldHint, + Callout, + Margins, +} from '@rocket.chat/fuselage'; +import { useDefaultSettingFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useTranslation, useSettingsDispatch } from '@rocket.chat/ui-contexts'; +import type { ChangeEvent } from 'react'; +import React, { Fragment } from 'react'; +import { useForm } from 'react-hook-form'; + +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; +import { useEditableSetting } from '../EditableSettingsContext'; +import Setting from '../settings/Setting'; +import SettingsGroupPageSkeleton from '../settings/SettingsGroupPage/SettingsGroupPageSkeleton'; + +const AdminFeaturePreviewPage = () => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const allowFeaturePreviewSetting = useEditableSetting('Accounts_AllowFeaturePreview'); + const { features } = useDefaultSettingFeaturePreviewList(); + + const { + watch, + formState: { isDirty }, + setValue, + handleSubmit, + reset, + } = useForm({ + defaultValues: { featuresPreview: features }, + }); + const { featuresPreview } = watch(); + const dispatch = useSettingsDispatch(); + + const handleSave = async () => { + try { + const featuresToBeSaved = featuresPreview.map((feature) => ({ name: feature.name, value: feature.value })); + + await dispatch([ + { _id: allowFeaturePreviewSetting!._id, value: allowFeaturePreviewSetting!.value }, + { _id: 'Accounts_Default_User_Preferences_featuresPreview', value: JSON.stringify(featuresToBeSaved) }, + ]); + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + reset({ featuresPreview }); + } + }; + + const handleFeatures = (e: ChangeEvent) => { + const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item)); + setValue('featuresPreview', updated, { shouldDirty: true }); + }; + + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); + + if (!allowFeaturePreviewSetting) { + // TODO: Implement FeaturePreviewSkeleton component + return ; + } + + return ( + + + + + + + {t('Feature_preview_admin_page_description')} + {t('Feature_preview_page_callout')} + {t('Feature_preview_admin_page_callout')} + + + + + {grouppedFeaturesPreview?.map(([group, features], index) => ( + + + {features.map((feature) => ( + + + + {t(feature.i18n)} + + + {feature.description && {t(feature.description)}} + + {feature.imageUrl && } + + ))} + + + ))} + + + + + + + + + + + ); +}; + +export default AdminFeaturePreviewPage; diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx new file mode 100644 index 000000000000..a7d6bd77d136 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -0,0 +1,26 @@ +import { usePermission } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { memo } from 'react'; + +import SettingsProvider from '../../../providers/SettingsProvider'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; +import EditableSettingsProvider from '../settings/EditableSettingsProvider'; +import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; + +const AdminFeaturePreviewRoute = (): ReactElement => { + const canViewFeaturesPreview = usePermission('manage-cloud'); + + if (!canViewFeaturesPreview) { + return ; + } + + return ( + + + + + + ); +}; + +export default memo(AdminFeaturePreviewRoute); diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index f70df1625871..d244d5e2f19b 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -104,6 +104,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: `/admin/subscription`; pattern: '/admin/subscription'; }; + 'admin-feature-preview': { + pathname: '/admin/feature-preview'; + pattern: '/admin/feature-preview'; + }; } } @@ -237,3 +241,8 @@ registerAdminRoute('/subscription', { name: 'subscription', component: lazy(() => import('./subscription/SubscriptionRoute')), }); + +registerAdminRoute('/feature-preview', { + name: 'admin-feature-preview', + component: lazy(() => import('./featurePreview/AdminFeaturePreviewRoute')), +}); diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index 013206d9e9a8..fc7d307396d4 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -1,3 +1,5 @@ +import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; + import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../app/authorization/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; @@ -129,6 +131,12 @@ export const { icon: 'emoji', permissionGranted: (): boolean => hasPermission('manage-emoji'), }, + { + href: '/admin/feature-preview', + i18nLabel: 'Feature_preview', + icon: 'flask', + permissionGranted: () => defaultFeaturesPreview?.length > 0, + }, { href: '/admin/settings', i18nLabel: 'Settings', diff --git a/apps/meteor/public/images/featurePreview/enhanced-navigation.png b/apps/meteor/public/images/featurePreview/enhanced-navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..4240326ba985d0a054ae5d9c8521b24c8d93e6f7 GIT binary patch literal 2372 zcmeHIYgCg*8lKz;k(*0VynsLp8%mX01@Ra$34#GB2w8)n2Lz6kXazzALMRv_YP;xG zVrfx?0*kHdhC)CtVh|D##7hAYg%TP_P1$fufB*?0CfSL{AKkOvo<04szxKyBGw<`h z^UgExH}gzEL^#D}&89T~0Gp8DKq>$zF&wv8TEKSeq4Rv$;nISm(*aoTVg3*yRQGuZ zBGRdp08rcKGzASbgB(T%puW)RWda5O%aM>k@?kbYDJ7;mXg7)%7}|_U^u0g_?-vIh z3PLLno>;U^yRy;Ip#U%U`gz?3Q9dbXM|jd~=gzm4M_kLikKsE!!V_rWmyy}L8oj)= zS2fb?Im$rG8QwM!!;_UE8Rx|HP0%OX2>m!4{!$M9@ zmzycKXS|vvx3jAFOuN;#LOx%@xz-bPQB~L4+B$H!?W8hJu;wTX4KMQ=yEwA+byHJQ z4Srntp0Hr<_>yuo%x4&B_O`IRa^*|1XD(Z^^k)!1hMgXbR43&u9)Lcv`iBcipXe%2 zq_vq)1Omaa`(22J@|`C&yL#jc-3GtCI}p2cuqKsjiPzA?P2zh4jxQVm;07eyMCq?+ zYV8fOwUa-ksVNIq0FcPLSd^mM+Od*DToGaEjy4EpBb~T5rBg*r>d@@<%gYepB7JF0 zqq_ZhTTbt4e99^Fl9eP{muC7SQ?As-T7ZUt1HXW7`egSV>XAs%dT2+~9d#BA(P*)j z(D0-Wc6F=Xq;Ez*19MWIbzS0osT7X6ozKk>&N3dh)U{&Re&fo8P%s&w0N~{VcrIKq z2x~}d4RQmV;DFu^gWmrCgjZEc@Ze_b(=Aoh-_d=XJZ8+v)lKA~z%BN~S86rpYMt?0u zFkC3)tSau`mnY25=B{g)dM3owSbf=Vw0-aZz2T~fTHkJ@F6pcLmqLRS&;Cv>BWR!I z+RPN!dq?ck6I|axSa+4|B7lNzG$S`NkbWu8%wj3c6P@vb)pL!5@#dmB^TW{?vN zBGfH=G3iH<==4p-aBGihHuu9{(zqJ-xB51NXv62_%8>>T@awGan@0K^3i((iN+xo%#ouHt&I7j7;gA~ zaM|kYft59-NVWnweEfy~obuR!X;>4?FtKhWk9LQc+ATc4!u|H0t|p=C4#)UkIs1b* zU~l>r;^|xj-Rs_fd+|eK`kzi4=TVAO6y`vwsAMd7!IrRlB4EwT%Irr}B_*-~AjVn(;{-)H)yq<^yYV&jM#%$9>QVh%U z%UJHItG`bwI}ZY}o0&klMYUn~qrAe2#ymoOtW#gr9Dz%HofNmdj`t8&#p7n_pOvuZ z=F?L2tj|bH_{0E*sl4L|c!c+jV` zeXy-WWIHdBNTLXs_UIBMDy?(d=pS%;{K~$(^7C(zfUTgQC0m#>rkYzG!&0srmo+zP z%uIJAf4!wl*sSe43silWOG&I~w3z|7=J<^(E=H1Jo*@0}lR+k4a4{Z+7t=?Z<-^16 z#HH**nNR$)vabV9KteLt5UV>qWh13{~`%a65CIDvwi8xxILa6 z!t*MO@WlMq>6uJgtPOPTCQBxzspEI!dI! CCsuy| literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png b/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..c36c7e44a29d8c27c9c035a77026f1cd7e7dabd0 GIT binary patch literal 4776 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 a2K(3rHj$^VY%~U(889)lFep3k9Q`kU29J{f literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/timestamp.png b/apps/meteor/public/images/featurePreview/timestamp.png new file mode 100644 index 0000000000000000000000000000000000000000..7573f97db55b73e2d880af67e6705e3c4a2040c7 GIT binary patch literal 51432 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

        &c?(AAUaFYNC38B5NhzYmJ_RR zL>SGF>SN$vDeeqMfUz1WeRyOj;Ilr%cMsNSwW*xQok;wu-W~Tik;lp>_`PH^@n4S@ zXw=7}@NQjEv>_%k*v;YbU*w%TtT+7+wmo;YUEI<%0sv)jAOjZT)wo=d6=sickEvhuX7{PxVy2T$f-kR=!SU=hVj!2k5|7`y#qK1k1)5N7xjMVQ-H4!NL*;UVF=FMdD!@J-yLk|D&~XB+;jk$v9i#aqNbKKbu^*4BVwni?4S&ceH%)78Vo;3We3z>0}G(1W=Te^H+ zc3x`TUiBlCK8kjO`)Jlb4l8GuDG**g!r+l8S7}78R6^WFWuoTL;6SIIbdQ7FC0>|Aqq~|&r=2%&UIMNuFHXGipg$|j6gaGa~R9GyX%`#=8mz& z3F%O4WFF_ttHHFgIZ+N&G2gE5GZpX*q=o-WEO)j_tD^PBBG=-7;r+*Q4Z2` zd)&d#IICHLqscicjx#W19KMHf0lpyLuck>DOZN<{4le$@2=v?aU+Q|4qM*k>-V3l4 zhz_qPiX%|Uj6NG1EbPn~E(IF^otw_5K0Zsnh!>(6!|GbJjcFu}_E&iooVwdg4L;Tg zgvRB3UbgDZ2{|RieK*w){5%dEz&s_U(5s7*4#wg50$1<-3H($4{;Ym7^$!&E6_lYf z*4BfZ5_qKWaWv4dWpk430t3i^55*3c(8Ukv_bVAV7a(#z84gq0an27F(A2H*4Db75 ziQ2P=`Vd`>>uP4kvi=Bs0lN|rw~3n*-QSY4=d8uY8|)@WgqMZq>2;;Cyz(o}R#s7|?Xb*2FxuR(TNhozRkI6;i zKV-ND5n7cS)KuR3=PGsvO%nVDlMhnUVk_Qz_>=HX7A6@#RmmB@|!RG9Y_ovVfRS7{@8n5Q6 zTwP2>0ytm%d`Sl~0RE+Z(~gIW&gOJ>$Zdl~yl|qPxCQ6YifqjhC}lzq-x`*MbDnxY z6X%@`=;opCoAxl|$!i}hty9``$(#9N2ch%%%8E?D0?^4*pA@1%!39QxJE4!H8Ym%X zLnYCKCu1c1-fZ)636;P86s0(JX}%CBXxfe_!TRN{FVItBs!p&af`C@3lG(C>Axw?8 z#WI65W$*}y2`%agC-7q7YkGmWpTpuoM#UvS4d3E>2umvtH*JA*g znd19VV?1e9@2OtR1~luQ!q#hzCKGejMFjj3_P`StljZ_yvDu~RnGF;wip%36cjm0# z^i3%$wt^!^+D;#A!_lRBY6#(Mb5r`;Zu`?+MP=-h00D&kQHPY=SC1M`uU0H3G%LD5 zGE>OQDaJS_maeT}vRz>!lmduAeeszNL1||4yV~8)Lg#}Se z<^M?=o8`9llaqG0di12T@@OCNmOMisOHhF8sFzLjD)*+ro!G7}DqkUv^FO{-GJA^j z-1;()hM&|Q5p_5dI!`o*ZgQMf%R0v+M0+@GI?4tpz^o0gEyvNsk*Dx0RFWV<&)xL| zBY_1)HeDcr)3p0>>}0qQAu-S$L)SlssV^~nQ*viu62k?@6&=T(d+244z|2w`r8ua!_t-e)Vw z0G?bZOYb0Z>f&?%7sgn3_v6QRgkji#fsAD$GHN<#kiOGaI1}g}uq_l8v`nyvdr6>7 zzAktxI{={0brzX+yig3qk?vDaa+x$5{=++v{>mFZ36cWHInd`P*p{A^ooC zdtt=0+$!gdR%*J&LlpI?U|&?9K{uG3&zUA|2k$m6DK_9OcHkdFJP0edE84t3f z87S=QiEo&HDD@z~d8Zb>-~1LD=A@bc@zA4&uI~E9cx{cLRo6iLG4~&gQWWTeH-n86 zVyL~(SP9{$h1*yiH`>weD92Vi`?R+Qk}()=fkp2{r|tDGJz68!M_Ju+$*xfQd9U9d zBr%QoLTWQ@!)aQYt-KFT=a|$K_8_v+sNZ%B%*ddN2wvCQonH|+p)fJN7fF-lR{P0) z@t(tlo8X6nAVSHo-Ai)O!N(AHwShk_Jw~Is-1+4u`$FY|_;wPiYy@JPYLuGPt&W5~ zjfJv>J$(lPaEW5Fp@H<&&ShceaQl?-;qbe6ds?;WC*hX(zyxT6tWn3Icmb%&8vH7U zq}cr6KT~41)4r%BcRr4wAL_kK z``AB~{bl+YQ6l*5f088l7kw7)(xO%?bGsbb6vqn9dPj{We|Aw5d~A8<`^L@Kqr5Pm zn@?#Ua|0q_+XPG+d~syb%T21hUZyOeAf!4c1!!E_9^HK{Iq$YVa!_oij2=;TT p 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 i18nLabel: 'Sidebar_Sections_Order', i18nDescription: 'Sidebar_Sections_Order_Description', }); + + await this.add('Accounts_Default_User_Preferences_featuresPreview', '[]', { + type: 'string', + public: true, + }); }); await this.section('Avatar', async function () { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index b8341f7c0994..d933f1f3c4b3 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -186,6 +186,7 @@ describe('miscellaneous', () => { 'muteFocusedConversations', 'notifyCalendarEvents', 'enableMobileRinging', + 'featuresPreview', ].filter((p) => Boolean(p)); expect(res.body).to.have.property('success', true); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 0e99c1bdc1d8..47d2034e002e 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -85,6 +85,7 @@ "Accounts_AllowEmailChange": "Allow Email Change", "Accounts_AllowEmailNotifications": "Allow Email Notifications", "Accounts_AllowFeaturePreview": "Allow Feature Preview", + "Accounts_AllowFeaturePreview_Description": "Make feature preview available to all workspace members.", "Accounts_AllowPasswordChange": "Allow Password Change", "Accounts_AllowPasswordChangeForOAuthUsers": "Allow Password Change for OAuth Users", "Accounts_AllowRealNameChange": "Allow Name Change", @@ -1125,8 +1126,8 @@ "Common_Access": "Common Access", "Commit": "Commit", "Community": "Community", - "Contextualbar_resizable": "Contextual bar resizable", - "Contextualbar_resizable_description": "Allows you to adjust the size of the contextual bar by simply dragging, giving you instant customization and flexibility", + "Contextualbar_resizable": "Resizable contextual bar", + "Contextualbar_resizable_description": "Adjust the size of the contextual bar by clicking and dragging the edge, giving you instant customization and flexibility.", "Free_Edition": "Free edition", "Composer_not_available_phone_calls": "Messages are not available on phone calls", "Condensed": "Condensed", @@ -1949,8 +1950,8 @@ "Enable_Password_History": "Enable Password History", "Enable_Password_History_Description": "When enabled, users won't be able to update their passwords to some of their most recently used passwords.", "Enable_Svg_Favicon": "Enable SVG favicon", - "Enable_timestamp": "Enable timestamp parsing in messages", - "Enable_timestamp_description": "Enable timestamps to be parsed in messages", + "Enable_timestamp": "Timestamp in messages", + "Enable_timestamp_description": "Render Unix timestamps inside messages in your local (system) timezone.", "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", @@ -2284,7 +2285,10 @@ "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", "Feature_preview": "Feature preview", - "Feature_preview_page_description": "Welcome to the features preview page! Here, you can enable the latest cutting-edge features that are currently under development and not yet officially released.\n\nPlease note that these configurations are still in the testing phase and may not be stable or fully functional.", + "Feature_preview_page_description": "Enable the latest features that are currently under development.", + "Feature_preview_page_callout": "Feature previews are being tested and may not be stable or fully functional. Features may become premium capabilities once officially released.", + "Feature_preview_admin_page_description": "Choose what feature previews to make available to workspace members.", + "Feature_preview_admin_page_callout": "Features enabled here will be enabled to each user in their feature preview preferences.", "featured": "featured", "Featured": "Featured", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings (Admin -> Video Conference).", @@ -4364,7 +4368,7 @@ "Queue_Time": "Queue Time", "Queue_management": "Queue Management", "Quick_reactions": "Quick reactions", - "Quick_reactions_description": "The three most used reactions get an easy access while your mouse is over the message", + "Quick_reactions_description": "Easily access your most used and most recent emoji message reactions by hovering on a message.", "quote": "quote", "Quote": "Quote", "Random": "Random", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 090e081e83fa..3110d6e82a67 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -2169,7 +2169,6 @@ "Favorite_Rooms": "पसंदीदा कमरे सक्षम करें", "Favorites": "पसंदीदा", "Feature_preview": "फ़ीचर पूर्वावलोकन", - "Feature_preview_page_description": "फीचर पूर्वावलोकन पृष्ठ पर आपका स्वागत है! यहां, आप नवीनतम अत्याधुनिक सुविधाओं को सक्षम कर सकते हैं जो वर्तमान में विकास के अधीन हैं और अभी तक आधिकारिक तौर पर जारी नहीं की गई हैं।\n\nकृपया ध्यान दें कि ये कॉन्फ़िगरेशन अभी भी परीक्षण चरण में हैं और स्थिर या पूरी तरह कार्यात्मक नहीं हो सकते हैं।", "featured": "प्रदर्शित", "Featured": "प्रदर्शित", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "यह सुविधा प्रशासन सेटिंग्स (एडमिन -> वीडियो कॉन्फ्रेंस) से सक्षम होने के लिए उपरोक्त चयनित कॉल प्रदाता पर निर्भर करती है।", @@ -4128,7 +4127,6 @@ "Queue_Time": "कतार समय", "Queue_management": "कतार प्रबंधन", "Quick_reactions": "त्वरित प्रतिक्रियाएँ", - "Quick_reactions_description": "जब आपका माउस संदेश पर होता है तो सबसे अधिक उपयोग की जाने वाली तीन प्रतिक्रियाओं तक आसान पहुंच मिलती है", "quote": "उद्धरण", "Quote": "उद्धरण", "Random": "Random", @@ -4987,7 +4985,7 @@ "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", "The_channel_name_is_required": "चैनल का नाम आवश्यक है", "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", - "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", @@ -6134,4 +6132,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} \ No newline at end of file +} diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx new file mode 100644 index 000000000000..eece30cc7280 --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx @@ -0,0 +1,21 @@ +import { Badge } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import { usePreferenceFeaturePreviewList } from '../../hooks/usePreferenceFeaturePreviewList'; + +const FeaturePreviewBadge = () => { + const t = useTranslation(); + const { unseenFeatures } = usePreferenceFeaturePreviewList(); + + if (!unseenFeatures) { + return null; + } + + return ( + + {unseenFeatures} + + ); +}; + +export default FeaturePreviewBadge; diff --git a/packages/ui-client/src/components/FeaturePreview/index.ts b/packages/ui-client/src/components/FeaturePreview/index.ts new file mode 100644 index 000000000000..f6b8e5f2071e --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/index.ts @@ -0,0 +1,2 @@ +export { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from './FeaturePreview'; +export { default as FeaturePreviewBadge } from './FeaturePreviewBadge'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 8642983229aa..7308c8e75431 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,7 +11,7 @@ export * as UserStatus from './UserStatus'; export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; -export * from './FeaturePreview/FeaturePreview'; +export * from './FeaturePreview'; export * from './RoomBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts new file mode 100644 index 000000000000..373862379cc1 --- /dev/null +++ b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts @@ -0,0 +1,12 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const useDefaultSettingFeaturePreviewList = () => { + const featurePreviewSettingJSON = useSetting('Accounts_Default_User_Preferences_featuresPreview'); + + const settingFeaturePreview = useMemo(() => parseSetting(featurePreviewSettingJSON), [featurePreviewSettingJSON]); + + return useFeaturePreviewList(settingFeaturePreview ?? []); +}; diff --git a/packages/ui-client/src/hooks/useFeaturePreview.ts b/packages/ui-client/src/hooks/useFeaturePreview.ts index 4bdda9c9251a..bd46adfdefff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreview.ts +++ b/packages/ui-client/src/hooks/useFeaturePreview.ts @@ -1,7 +1,8 @@ -import { type FeaturesAvailable, useFeaturePreviewList } from './useFeaturePreviewList'; +import { type FeaturesAvailable } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; export const useFeaturePreview = (featureName: FeaturesAvailable) => { - const { features } = useFeaturePreviewList(); + const { features } = usePreferenceFeaturePreviewList(); const currentFeature = features?.find((feature) => feature.name === featureName); diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index ff103a8d84ef..08bda4ff81ff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -1,5 +1,4 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; export type FeaturesAvailable = | 'quickReactions' @@ -24,6 +23,7 @@ export type FeaturePreviewProps = { }; }; +// TODO: Move the features preview array to another directory to be accessed from both BE and FE. export const defaultFeaturesPreview: FeaturePreviewProps[] = [ { name: 'quickReactions', @@ -47,6 +47,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Enable_timestamp', description: 'Enable_timestamp_description', group: 'Message', + imageUrl: 'images/featurePreview/timestamp.png', value: false, enabled: true, }, @@ -55,6 +56,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Contextualbar_resizable', description: 'Contextualbar_resizable_description', group: 'Navigation', + imageUrl: 'images/featurePreview/resizable-contextual-bar.png', value: false, enabled: true, }, @@ -63,6 +65,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'New_navigation', description: 'New_navigation_description', group: 'Navigation', + imageUrl: 'images/featurePreview/enhanced-navigation.png', value: false, enabled: true, }, @@ -82,22 +85,27 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ export const enabledDefaultFeatures = defaultFeaturesPreview.filter((feature) => feature.enabled); -export const useFeaturePreviewList = () => { - const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); - const userFeaturesPreview = useUserPreference('featuresPreview'); - - if (!featurePreviewEnabled) { - return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; +// TODO: Remove this logic after we have a way to store object settings. +export const parseSetting = (setting?: FeaturePreviewProps[] | string) => { + if (typeof setting === 'string') { + try { + return JSON.parse(setting) as FeaturePreviewProps[]; + } catch (_) { + return; + } } + return setting; +}; +export const useFeaturePreviewList = (featuresList: Pick[]) => { const unseenFeatures = enabledDefaultFeatures.filter( - (feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name), + (defaultFeature) => !featuresList?.find((feature) => feature.name === defaultFeature.name), ).length; - const mergedFeatures = enabledDefaultFeatures.map((feature) => { - const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name); - return { ...feature, ...userFeature }; + const mergedFeatures = enabledDefaultFeatures.map((defaultFeature) => { + const features = featuresList?.find((feature) => feature.name === defaultFeature.name); + return { ...defaultFeature, ...features }; }); - return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled }; + return { unseenFeatures, features: mergedFeatures }; }; diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx similarity index 79% rename from packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx rename to packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx index e348cfb6a864..ac3d6f92d51a 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx @@ -1,10 +1,11 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { renderHook } from '@testing-library/react'; -import { useFeaturePreviewList, enabledDefaultFeatures } from './useFeaturePreviewList'; +import { enabledDefaultFeatures } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; it('should return the number of unseen features and Accounts_AllowFeaturePreview enabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', true).build(), }); @@ -18,7 +19,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return the number of unseen features and Accounts_AllowFeaturePreview disabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', false).build(), }); @@ -32,7 +33,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return 0 unseen features', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -49,7 +50,7 @@ it('should return 0 unseen features', () => { }); it('should ignore removed feature previews', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -72,7 +73,7 @@ it('should ignore removed feature previews', () => { }); it('should turn off ignored feature previews', async () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) diff --git a/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts new file mode 100644 index 000000000000..d7c4c13417d2 --- /dev/null +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts @@ -0,0 +1,16 @@ +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { FeaturePreviewProps, parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const usePreferenceFeaturePreviewList = () => { + const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); + const userFeaturesPreviewPreference = useUserPreference('featuresPreview'); + const userFeaturesPreview = useMemo(() => parseSetting(userFeaturesPreviewPreference), [userFeaturesPreviewPreference]); + const { unseenFeatures, features } = useFeaturePreviewList(userFeaturesPreview ?? []); + + if (!featurePreviewEnabled) { + return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; + } + return { unseenFeatures, features, featurePreviewEnabled }; +}; diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts index 3e640343da5b..a96ef265aadc 100644 --- a/packages/ui-client/src/index.ts +++ b/packages/ui-client/src/index.ts @@ -1,5 +1,7 @@ export * from './components'; export * from './hooks/useFeaturePreview'; +export * from './hooks/useDefaultSettingFeaturePreviewList'; export * from './hooks/useFeaturePreviewList'; +export * from './hooks/usePreferenceFeaturePreviewList'; export * from './hooks/useDocumentTitle'; export * from './helpers'; From a4fcd99e3006f28abb0b3f06985809b447365fc4 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Tue, 24 Sep 2024 12:45:44 -0300 Subject: [PATCH 40/57] chore: move common files to core-services (#33341) --- _templates/service/new/service.ejs.t | 8 ++------ ee/apps/account-service/src/service.ts | 8 ++------ ee/apps/authorization-service/src/service.ts | 8 ++------ ee/apps/ddp-streamer/src/service.ts | 8 ++------ ee/apps/omnichannel-transcript/src/service.ts | 8 ++------ ee/apps/presence-service/src/service.ts | 8 ++------ ee/apps/queue-worker/src/service.ts | 8 ++------ ee/apps/stream-hub-service/src/service.ts | 8 ++------ packages/core-services/src/index.ts | 2 ++ .../core-services/src/lib}/mongo.ts | 14 ++------------ 10 files changed, 20 insertions(+), 60 deletions(-) rename {apps/meteor/ee/server/services => packages/core-services/src/lib}/mongo.ts (72%) diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 77f02d2b7769..699539365259 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -1,12 +1,10 @@ --- to: ee/apps/<%= name %>/src/service.ts --- -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; const PORT = process.env.PORT || <%= h.random() %>; @@ -14,9 +12,7 @@ const PORT = process.env.PORT || <%= h.random() %>; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/account-service/src/service.ts b/ee/apps/account-service/src/service.ts index 07ca30ed748f..c2f64e37bde3 100755 --- a/ee/apps/account-service/src/service.ts +++ b/ee/apps/account-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3033; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/authorization-service/src/service.ts b/ee/apps/authorization-service/src/service.ts index 4dcd466afa60..1698ef7a115c 100755 --- a/ee/apps/authorization-service/src/service.ts +++ b/ee/apps/authorization-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3034; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/ddp-streamer/src/service.ts b/ee/apps/ddp-streamer/src/service.ts index 07666a265dbe..58552240cadd 100755 --- a/ee/apps/ddp-streamer/src/service.ts +++ b/ee/apps/ddp-streamer/src/service.ts @@ -1,16 +1,12 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts index 66456456fb74..ad60687d5ba4 100644 --- a/ee/apps/omnichannel-transcript/src/service.ts +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -1,20 +1,16 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3036; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/presence-service/src/service.ts b/ee/apps/presence-service/src/service.ts index 0e1c97f2daa2..0c51c30dc577 100755 --- a/ee/apps/presence-service/src/service.ts +++ b/ee/apps/presence-service/src/service.ts @@ -1,19 +1,15 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3031; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts index 4bc6c9642913..c11376d56534 100644 --- a/ee/apps/queue-worker/src/service.ts +++ b/ee/apps/queue-worker/src/service.ts @@ -1,20 +1,16 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; const PORT = process.env.PORT || 3038; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/ee/apps/stream-hub-service/src/service.ts b/ee/apps/stream-hub-service/src/service.ts index eade703321d2..5e035548dc38 100755 --- a/ee/apps/stream-hub-service/src/service.ts +++ b/ee/apps/stream-hub-service/src/service.ts @@ -1,11 +1,9 @@ -import { api } from '@rocket.chat/core-services'; +import { api, getConnection, getTrashCollection } from '@rocket.chat/core-services'; import { Logger } from '@rocket.chat/logger'; import { broker } from '@rocket.chat/network-broker'; -import type { Document } from 'mongodb'; import polka from 'polka'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { DatabaseWatcher } from '../../../../apps/meteor/server/database/DatabaseWatcher'; import { StreamHub } from './StreamHub'; @@ -14,9 +12,7 @@ const PORT = process.env.PORT || 3035; (async () => { const db = await getConnection(); - const trash = await getCollection(Collections.Trash); - - registerServiceModels(db, trash); + registerServiceModels(db, await getTrashCollection()); api.setBroker(broker); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 8eea19ea7405..85722c98839f 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -75,6 +75,8 @@ export { AnalyticsOverviewDataResult, } from './types/IOmnichannelAnalyticsService'; +export { getConnection, getTrashCollection } from './lib/mongo'; + export { AutoUpdateRecord, FindVoipRoomsParams, diff --git a/apps/meteor/ee/server/services/mongo.ts b/packages/core-services/src/lib/mongo.ts similarity index 72% rename from apps/meteor/ee/server/services/mongo.ts rename to packages/core-services/src/lib/mongo.ts index 27e9e931c039..fab1fd108d99 100644 --- a/apps/meteor/ee/server/services/mongo.ts +++ b/packages/core-services/src/lib/mongo.ts @@ -5,16 +5,6 @@ const { MONGO_URL = 'mongodb://localhost:27017/rocketchat' } = process.env; const name = /^mongodb:\/\/.*?(?::[0-9]+)?\/([^?]*)/.exec(MONGO_URL)?.[1]; -export enum Collections { - Subscriptions = 'rocketchat_subscription', - UserSession = 'usersSessions', - User = 'users', - Trash = 'rocketchat__trash', - Messages = 'rocketchat_message', - Rooms = 'rocketchat_room', - Settings = 'rocketchat_settings', -} - function connectDb(options?: MongoClientOptions): Promise { const client = new MongoClient(MONGO_URL, options); @@ -44,9 +34,9 @@ export const getConnection = ((): ((options?: MongoClientOptions) => Promise }; })(); -export async function getCollection(name: Collections): Promise> { +export async function getTrashCollection(): Promise> { if (!db) { db = await getConnection(); } - return db.collection(name); + return db.collection('rocketchat__trash'); } From d94a159126a757d156496f8ab2ce95f09ea45ff1 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Sep 2024 12:50:10 -0300 Subject: [PATCH 41/57] regression: `Sidepanel` color highlight (#33342) --- apps/meteor/package.json | 2 +- apps/uikit-playground/package.json | 2 +- ee/packages/ui-theming/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/gazzodown/package.json | 2 +- packages/ui-avatar/package.json | 2 +- packages/ui-client/package.json | 2 +- packages/ui-composer/package.json | 2 +- packages/ui-video-conf/package.json | 2 +- yarn.lock | 50 +++++++++++++-------------- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a09dcb657466..a30ce30a7b6f 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -241,7 +241,7 @@ "@rocket.chat/favicon": "workspace:^", "@rocket.chat/forked-matrix-appservice-bridge": "^4.0.2", "@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.3", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.33.0", diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 48c099b8d9d1..c5e7628001c6 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -17,7 +17,7 @@ "@lezer/highlight": "^1.1.6", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/fuselage-toastbar": "^0.33.0", diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index 0548f235c3fd..29fe229ca532 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/ui-contexts": "workspace:~", diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 8fb0ca254d68..573d65dc215e 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -66,7 +66,7 @@ "@rocket.chat/apps-engine": "1.45.0-alpha.868", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "~0.38.0", diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 47bf2782226e..a5883bbe3c0f 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -28,7 +28,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-tokens": "^0.33.1", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:^", diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index a32a7a8be6bf..70f0efe2cff9 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -4,7 +4,7 @@ "private": true, "devDependencies": { "@babel/core": "~7.22.20", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~17.0.80", "@types/react-dom": "~17.0.25", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 82705ba026cb..40bccd8ca628 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -21,7 +21,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 363299698bfa..c43f3485880e 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -19,7 +19,7 @@ "@babel/core": "~7.22.20", "@react-aria/toolbar": "^3.0.0-beta.1", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/icons": "~0.38.0", "@storybook/addon-actions": "~6.5.16", "@storybook/addon-docs": "~6.5.16", diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 0f1f3e50eab8..74c3ec43bcbb 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@rocket.chat/css-in-js": "~0.31.25", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.0", + "@rocket.chat/fuselage": "^0.59.1", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/icons": "~0.38.0", "@rocket.chat/jest-presets": "workspace:~", diff --git a/yarn.lock b/yarn.lock index c0fb2733310e..20c005cca203 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8910,7 +8910,7 @@ __metadata: "@rocket.chat/apps-engine": 1.45.0-alpha.868 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/gazzodown": "workspace:^" @@ -8959,19 +8959,19 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-avatar": 7.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": "*" + "@rocket.chat/ui-video-conf": 11.0.0-rc.0 "@tanstack/react-query": "*" react: "*" react-dom: "*" languageName: unknown linkType: soft -"@rocket.chat/fuselage@npm:^0.59.0": - version: 0.59.0 - resolution: "@rocket.chat/fuselage@npm:0.59.0" +"@rocket.chat/fuselage@npm:^0.59.1": + version: 0.59.1 + resolution: "@rocket.chat/fuselage@npm:0.59.1" dependencies: "@rocket.chat/css-in-js": ^0.31.25 "@rocket.chat/css-supports": ^0.31.25 @@ -8989,7 +8989,7 @@ __metadata: react: ^17.0.2 react-dom: ^17.0.2 react-virtuoso: 1.2.4 - checksum: 259dce5381a3c3e0d7c7f3dc7ab51346cb65a9f4906a5ca5d6a976627d05e01e7f8a3a940604d0ad1b2b4ed89c250a871ef3fb253f6bbb69d35bc931e193898d + checksum: 6ecceaefe8b2c6b9fe0ba3b6e19360f5d46af9697e7c97af16cce6c9eea2cb79255f38ccffdbc4766b4104c079281b4980fe7c924cd62fe5d2d341581fdf4f62 languageName: node linkType: hard @@ -9000,7 +9000,7 @@ __metadata: "@babel/core": ~7.22.20 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-tokens": ^0.33.1 "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:^" @@ -9047,10 +9047,10 @@ __metadata: "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" - "@rocket.chat/message-parser": 0.31.29 + "@rocket.chat/message-parser": 0.31.30-rc.0 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-client": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 katex: "*" react: "*" languageName: unknown @@ -9372,7 +9372,7 @@ __metadata: "@rocket.chat/favicon": "workspace:^" "@rocket.chat/forked-matrix-appservice-bridge": ^4.0.2 "@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.3 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.33.0 @@ -10244,7 +10244,7 @@ __metadata: resolution: "@rocket.chat/ui-avatar@workspace:packages/ui-avatar" dependencies: "@babel/core": ~7.22.20 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/ui-contexts": "workspace:^" "@types/react": ~17.0.80 "@types/react-dom": ~17.0.25 @@ -10257,7 +10257,7 @@ __metadata: typescript: ~5.5.4 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-contexts": 11.0.0-rc.0 react: ~17.0.2 languageName: unknown linkType: soft @@ -10269,7 +10269,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" @@ -10308,8 +10308,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-avatar": 7.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 react: "*" react-i18next: "*" languageName: unknown @@ -10322,7 +10322,7 @@ __metadata: "@babel/core": ~7.22.20 "@react-aria/toolbar": ^3.0.0-beta.1 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/icons": ~0.38.0 "@storybook/addon-actions": ~6.5.16 "@storybook/addon-docs": ~6.5.16 @@ -10416,7 +10416,7 @@ __metadata: resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming" dependencies: "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/ui-contexts": "workspace:~" @@ -10446,7 +10446,7 @@ __metadata: "@rocket.chat/css-in-js": ~0.31.25 "@rocket.chat/emitter": ~0.31.25 "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/icons": ~0.38.0 "@rocket.chat/jest-presets": "workspace:~" @@ -10478,8 +10478,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": "*" - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-avatar": 7.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.0 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10495,7 +10495,7 @@ __metadata: "@lezer/highlight": ^1.1.6 "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/css-in-js": ~0.31.25 - "@rocket.chat/fuselage": ^0.59.0 + "@rocket.chat/fuselage": ^0.59.1 "@rocket.chat/fuselage-hooks": ^0.33.1 "@rocket.chat/fuselage-polyfills": ~0.31.25 "@rocket.chat/fuselage-toastbar": ^0.33.0 @@ -10567,7 +10567,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": "*" + "@rocket.chat/ui-contexts": 11.0.0-rc.0 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 2f9eea03d2122e963bc89559e772bf00a54ad200 Mon Sep 17 00:00:00 2001 From: Lucas Pelegrino Date: Tue, 24 Sep 2024 09:54:31 -0300 Subject: [PATCH 42/57] feat: Adds new admin feature preview setting management (#33212) Co-authored-by: Guilherme Gazzo --- .changeset/quick-rings-wave.md | 7 + .../UserMenu/hooks/useAccountItems.tsx | 4 +- .../hooks/useFeaturePreviewEnableQuery.ts | 28 ++++ .../sidebar/header/hooks/useAccountItems.tsx | 4 +- .../AccountFeaturePreviewBadge.tsx | 21 --- .../AccountFeaturePreviewPage.tsx | 44 ++---- .../client/views/account/sidebarItems.tsx | 5 +- .../AdminFeaturePreviewPage.tsx | 127 ++++++++++++++++++ .../AdminFeaturePreviewRoute.tsx | 26 ++++ apps/meteor/client/views/admin/routes.tsx | 9 ++ .../meteor/client/views/admin/sidebarItems.ts | 8 ++ .../featurePreview/enhanced-navigation.png | Bin 0 -> 2372 bytes .../resizable-contextual-bar.png | Bin 0 -> 4776 bytes .../images/featurePreview/timestamp.png | Bin 0 -> 51432 bytes apps/meteor/server/settings/accounts.ts | 5 + .../tests/end-to-end/api/miscellaneous.ts | 1 + packages/i18n/src/locales/en.i18n.json | 16 ++- packages/i18n/src/locales/hi-IN.i18n.json | 6 +- .../FeaturePreview/FeaturePreviewBadge.tsx | 21 +++ .../src/components/FeaturePreview/index.ts | 2 + packages/ui-client/src/components/index.ts | 2 +- .../useDefaultSettingFeaturePreviewList.ts | 12 ++ .../ui-client/src/hooks/useFeaturePreview.ts | 5 +- .../src/hooks/useFeaturePreviewList.ts | 32 +++-- ... usePreferenceFeaturePreviewList.spec.tsx} | 13 +- .../hooks/usePreferenceFeaturePreviewList.ts | 16 +++ packages/ui-client/src/index.ts | 2 + 27 files changed, 324 insertions(+), 92 deletions(-) create mode 100644 .changeset/quick-rings-wave.md create mode 100644 apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts delete mode 100644 apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx create mode 100644 apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx create mode 100644 apps/meteor/public/images/featurePreview/enhanced-navigation.png create mode 100644 apps/meteor/public/images/featurePreview/resizable-contextual-bar.png create mode 100644 apps/meteor/public/images/featurePreview/timestamp.png create mode 100644 packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx create mode 100644 packages/ui-client/src/components/FeaturePreview/index.ts create mode 100644 packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts rename packages/ui-client/src/hooks/{useFeaturePreviewList.spec.tsx => usePreferenceFeaturePreviewList.spec.tsx} (79%) create mode 100644 packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts diff --git a/.changeset/quick-rings-wave.md b/.changeset/quick-rings-wave.md new file mode 100644 index 000000000000..0ea22897ff45 --- /dev/null +++ b/.changeset/quick-rings-wave.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/i18n": minor +"@rocket.chat/ui-client": minor +--- + +Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. diff --git a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx index 82c39c5c1b10..e54e2b72d675 100644 --- a/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx +++ b/apps/meteor/client/NavBarV2/NavBarSettingsToolbar/UserMenu/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useEffectEvent(() => { router.navigate('/account'); diff --git a/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts new file mode 100644 index 000000000000..fd88f0237d29 --- /dev/null +++ b/apps/meteor/client/hooks/useFeaturePreviewEnableQuery.ts @@ -0,0 +1,28 @@ +import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; +import { useMemo } from 'react'; + +const handleFeaturePreviewEnableQuery = (item: FeaturePreviewProps, _: any, features: FeaturePreviewProps[]) => { + if (item.enableQuery) { + const expected = item.enableQuery.value; + const received = features.find((el) => el.name === item.enableQuery?.name)?.value; + if (expected !== received) { + item.disabled = true; + item.value = false; + } else { + item.disabled = false; + } + } + return item; +}; + +const groupFeaturePreview = (features: FeaturePreviewProps[]) => + Object.entries( + features.reduce((result, currentValue) => { + (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); + return result; + }, {} as Record), + ); + +export const useFeaturePreviewEnableQuery = (features: FeaturePreviewProps[]) => { + return useMemo(() => groupFeaturePreview(features.map(handleFeaturePreviewEnableQuery)), [features]); +}; diff --git a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx index 2be6b2b1dea2..51ab7a198a67 100644 --- a/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx +++ b/apps/meteor/client/sidebar/header/hooks/useAccountItems.tsx @@ -1,7 +1,7 @@ import { Badge } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { defaultFeaturesPreview, useFeaturePreviewList } from '@rocket.chat/ui-client'; import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; @@ -9,7 +9,7 @@ export const useAccountItems = (): GenericMenuItemProps[] => { const t = useTranslation(); const router = useRouter(); - const { unseenFeatures, featurePreviewEnabled } = useFeaturePreviewList(); + const { unseenFeatures, featurePreviewEnabled } = usePreferenceFeaturePreviewList(); const handleMyAccount = useMutableCallback(() => { router.navigate('/account'); diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx deleted file mode 100644 index c109ca1aefb5..000000000000 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewBadge.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Badge } from '@rocket.chat/fuselage'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; -import React from 'react'; -import { useTranslation } from 'react-i18next'; - -const AccountFeaturePreviewBadge = () => { - const { t } = useTranslation(); - const { unseenFeatures } = useFeaturePreviewList(); - - if (!unseenFeatures) { - return null; - } - - return ( - - {unseenFeatures} - - ); -}; - -export default AccountFeaturePreviewBadge; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index dd9ab6a90959..358d2394003b 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -1,4 +1,3 @@ -import { css } from '@rocket.chat/css-in-js'; import { ButtonGroup, Button, @@ -13,9 +12,10 @@ import { FieldLabel, FieldRow, FieldHint, + Callout, + Margins, } from '@rocket.chat/fuselage'; -import type { FeaturePreviewProps } from '@rocket.chat/ui-client'; -import { useFeaturePreviewList } from '@rocket.chat/ui-client'; +import { usePreferenceFeaturePreviewList } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { ChangeEvent } from 'react'; @@ -23,26 +23,12 @@ import React, { useEffect, Fragment } from 'react'; import { useForm } from 'react-hook-form'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; -const handleEnableQuery = (features: FeaturePreviewProps[]) => { - return features.map((item) => { - if (item.enableQuery) { - const expected = item.enableQuery.value; - const received = features.find((el) => el.name === item.enableQuery?.name)?.value; - if (expected !== received) { - item.disabled = true; - item.value = false; - } else { - item.disabled = false; - } - } - return item; - }); -}; const AccountFeaturePreviewPage = () => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const { features, unseenFeatures } = useFeaturePreviewList(); + const { features, unseenFeatures } = usePreferenceFeaturePreviewList(); const setUserPreferences = useEndpoint('POST', '/v1/users.setPreferences'); @@ -85,12 +71,7 @@ const AccountFeaturePreviewPage = () => { setValue('featuresPreview', updated, { shouldDirty: true }); }; - const grouppedFeaturesPreview = Object.entries( - handleEnableQuery(featuresPreview).reduce((result, currentValue) => { - (result[currentValue.group] = result[currentValue.group] || []).push(currentValue); - return result; - }, {} as Record), - ); + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); return ( @@ -105,14 +86,11 @@ const AccountFeaturePreviewPage = () => { )} {featuresPreview.length > 0 && ( <> - - {t('Feature_preview_page_description')} + + + {t('Feature_preview_page_description')} + {t('Feature_preview_page_callout')} + {grouppedFeaturesPreview?.map(([group, features], index) => ( diff --git a/apps/meteor/client/views/account/sidebarItems.tsx b/apps/meteor/client/views/account/sidebarItems.tsx index ca0376be329d..fa2ab8bd5e40 100644 --- a/apps/meteor/client/views/account/sidebarItems.tsx +++ b/apps/meteor/client/views/account/sidebarItems.tsx @@ -1,10 +1,9 @@ -import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; +import { defaultFeaturesPreview, FeaturePreviewBadge } from '@rocket.chat/ui-client'; import React from 'react'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client'; import { settings } from '../../../app/settings/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; -import AccountFeaturePreviewBadge from './featurePreview/AccountFeaturePreviewBadge'; export const { registerSidebarItem: registerAccountSidebarItem, @@ -54,7 +53,7 @@ export const { href: '/account/feature-preview', i18nLabel: 'Feature_preview', icon: 'flask', - badge: () => , + badge: () => , permissionGranted: () => settings.get('Accounts_AllowFeaturePreview') && defaultFeaturesPreview?.length > 0, }, { diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx new file mode 100644 index 000000000000..615fd20cf5a6 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewPage.tsx @@ -0,0 +1,127 @@ +import { + ButtonGroup, + Button, + Box, + ToggleSwitch, + Accordion, + Field, + FieldGroup, + FieldLabel, + FieldRow, + FieldHint, + Callout, + Margins, +} from '@rocket.chat/fuselage'; +import { useDefaultSettingFeaturePreviewList } from '@rocket.chat/ui-client'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; +import { useToastMessageDispatch, useTranslation, useSettingsDispatch } from '@rocket.chat/ui-contexts'; +import type { ChangeEvent } from 'react'; +import React, { Fragment } from 'react'; +import { useForm } from 'react-hook-form'; + +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; +import { useFeaturePreviewEnableQuery } from '../../../hooks/useFeaturePreviewEnableQuery'; +import { useEditableSetting } from '../EditableSettingsContext'; +import Setting from '../settings/Setting'; +import SettingsGroupPageSkeleton from '../settings/SettingsGroupPage/SettingsGroupPageSkeleton'; + +const AdminFeaturePreviewPage = () => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const allowFeaturePreviewSetting = useEditableSetting('Accounts_AllowFeaturePreview'); + const { features } = useDefaultSettingFeaturePreviewList(); + + const { + watch, + formState: { isDirty }, + setValue, + handleSubmit, + reset, + } = useForm({ + defaultValues: { featuresPreview: features }, + }); + const { featuresPreview } = watch(); + const dispatch = useSettingsDispatch(); + + const handleSave = async () => { + try { + const featuresToBeSaved = featuresPreview.map((feature) => ({ name: feature.name, value: feature.value })); + + await dispatch([ + { _id: allowFeaturePreviewSetting!._id, value: allowFeaturePreviewSetting!.value }, + { _id: 'Accounts_Default_User_Preferences_featuresPreview', value: JSON.stringify(featuresToBeSaved) }, + ]); + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } finally { + reset({ featuresPreview }); + } + }; + + const handleFeatures = (e: ChangeEvent) => { + const updated = featuresPreview.map((item) => (item.name === e.target.name ? { ...item, value: e.target.checked } : item)); + setValue('featuresPreview', updated, { shouldDirty: true }); + }; + + const grouppedFeaturesPreview = useFeaturePreviewEnableQuery(featuresPreview); + + if (!allowFeaturePreviewSetting) { + // TODO: Implement FeaturePreviewSkeleton component + return ; + } + + return ( + + + + + + + {t('Feature_preview_admin_page_description')} + {t('Feature_preview_page_callout')} + {t('Feature_preview_admin_page_callout')} + + + + + {grouppedFeaturesPreview?.map(([group, features], index) => ( + + + {features.map((feature) => ( + + + + {t(feature.i18n)} + + + {feature.description && {t(feature.description)}} + + {feature.imageUrl && } + + ))} + + + ))} + + + + + + + + + + + ); +}; + +export default AdminFeaturePreviewPage; diff --git a/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx new file mode 100644 index 000000000000..a7d6bd77d136 --- /dev/null +++ b/apps/meteor/client/views/admin/featurePreview/AdminFeaturePreviewRoute.tsx @@ -0,0 +1,26 @@ +import { usePermission } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React, { memo } from 'react'; + +import SettingsProvider from '../../../providers/SettingsProvider'; +import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; +import EditableSettingsProvider from '../settings/EditableSettingsProvider'; +import AdminFeaturePreviewPage from './AdminFeaturePreviewPage'; + +const AdminFeaturePreviewRoute = (): ReactElement => { + const canViewFeaturesPreview = usePermission('manage-cloud'); + + if (!canViewFeaturesPreview) { + return ; + } + + return ( + + + + + + ); +}; + +export default memo(AdminFeaturePreviewRoute); diff --git a/apps/meteor/client/views/admin/routes.tsx b/apps/meteor/client/views/admin/routes.tsx index f70df1625871..d244d5e2f19b 100644 --- a/apps/meteor/client/views/admin/routes.tsx +++ b/apps/meteor/client/views/admin/routes.tsx @@ -104,6 +104,10 @@ declare module '@rocket.chat/ui-contexts' { pathname: `/admin/subscription`; pattern: '/admin/subscription'; }; + 'admin-feature-preview': { + pathname: '/admin/feature-preview'; + pattern: '/admin/feature-preview'; + }; } } @@ -237,3 +241,8 @@ registerAdminRoute('/subscription', { name: 'subscription', component: lazy(() => import('./subscription/SubscriptionRoute')), }); + +registerAdminRoute('/feature-preview', { + name: 'admin-feature-preview', + component: lazy(() => import('./featurePreview/AdminFeaturePreviewRoute')), +}); diff --git a/apps/meteor/client/views/admin/sidebarItems.ts b/apps/meteor/client/views/admin/sidebarItems.ts index 013206d9e9a8..fc7d307396d4 100644 --- a/apps/meteor/client/views/admin/sidebarItems.ts +++ b/apps/meteor/client/views/admin/sidebarItems.ts @@ -1,3 +1,5 @@ +import { defaultFeaturesPreview } from '@rocket.chat/ui-client'; + import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../app/authorization/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; @@ -129,6 +131,12 @@ export const { icon: 'emoji', permissionGranted: (): boolean => hasPermission('manage-emoji'), }, + { + href: '/admin/feature-preview', + i18nLabel: 'Feature_preview', + icon: 'flask', + permissionGranted: () => defaultFeaturesPreview?.length > 0, + }, { href: '/admin/settings', i18nLabel: 'Settings', diff --git a/apps/meteor/public/images/featurePreview/enhanced-navigation.png b/apps/meteor/public/images/featurePreview/enhanced-navigation.png new file mode 100644 index 0000000000000000000000000000000000000000..4240326ba985d0a054ae5d9c8521b24c8d93e6f7 GIT binary patch literal 2372 zcmeHIYgCg*8lKz;k(*0VynsLp8%mX01@Ra$34#GB2w8)n2Lz6kXazzALMRv_YP;xG zVrfx?0*kHdhC)CtVh|D##7hAYg%TP_P1$fufB*?0CfSL{AKkOvo<04szxKyBGw<`h z^UgExH}gzEL^#D}&89T~0Gp8DKq>$zF&wv8TEKSeq4Rv$;nISm(*aoTVg3*yRQGuZ zBGRdp08rcKGzASbgB(T%puW)RWda5O%aM>k@?kbYDJ7;mXg7)%7}|_U^u0g_?-vIh z3PLLno>;U^yRy;Ip#U%U`gz?3Q9dbXM|jd~=gzm4M_kLikKsE!!V_rWmyy}L8oj)= zS2fb?Im$rG8QwM!!;_UE8Rx|HP0%OX2>m!4{!$M9@ zmzycKXS|vvx3jAFOuN;#LOx%@xz-bPQB~L4+B$H!?W8hJu;wTX4KMQ=yEwA+byHJQ z4Srntp0Hr<_>yuo%x4&B_O`IRa^*|1XD(Z^^k)!1hMgXbR43&u9)Lcv`iBcipXe%2 zq_vq)1Omaa`(22J@|`C&yL#jc-3GtCI}p2cuqKsjiPzA?P2zh4jxQVm;07eyMCq?+ zYV8fOwUa-ksVNIq0FcPLSd^mM+Od*DToGaEjy4EpBb~T5rBg*r>d@@<%gYepB7JF0 zqq_ZhTTbt4e99^Fl9eP{muC7SQ?As-T7ZUt1HXW7`egSV>XAs%dT2+~9d#BA(P*)j z(D0-Wc6F=Xq;Ez*19MWIbzS0osT7X6ozKk>&N3dh)U{&Re&fo8P%s&w0N~{VcrIKq z2x~}d4RQmV;DFu^gWmrCgjZEc@Ze_b(=Aoh-_d=XJZ8+v)lKA~z%BN~S86rpYMt?0u zFkC3)tSau`mnY25=B{g)dM3owSbf=Vw0-aZz2T~fTHkJ@F6pcLmqLRS&;Cv>BWR!I z+RPN!dq?ck6I|axSa+4|B7lNzG$S`NkbWu8%wj3c6P@vb)pL!5@#dmB^TW{?vN zBGfH=G3iH<==4p-aBGihHuu9{(zqJ-xB51NXv62_%8>>T@awGan@0K^3i((iN+xo%#ouHt&I7j7;gA~ zaM|kYft59-NVWnweEfy~obuR!X;>4?FtKhWk9LQc+ATc4!u|H0t|p=C4#)UkIs1b* zU~l>r;^|xj-Rs_fd+|eK`kzi4=TVAO6y`vwsAMd7!IrRlB4EwT%Irr}B_*-~AjVn(;{-)H)yq<^yYV&jM#%$9>QVh%U z%UJHItG`bwI}ZY}o0&klMYUn~qrAe2#ymoOtW#gr9Dz%HofNmdj`t8&#p7n_pOvuZ z=F?L2tj|bH_{0E*sl4L|c!c+jV` zeXy-WWIHdBNTLXs_UIBMDy?(d=pS%;{K~$(^7C(zfUTgQC0m#>rkYzG!&0srmo+zP z%uIJAf4!wl*sSe43silWOG&I~w3z|7=J<^(E=H1Jo*@0}lR+k4a4{Z+7t=?Z<-^16 z#HH**nNR$)vabV9KteLt5UV>qWh13{~`%a65CIDvwi8xxILa6 z!t*MO@WlMq>6uJgtPOPTCQBxzspEI!dI! CCsuy| literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png b/apps/meteor/public/images/featurePreview/resizable-contextual-bar.png new file mode 100644 index 0000000000000000000000000000000000000000..c36c7e44a29d8c27c9c035a77026f1cd7e7dabd0 GIT binary patch literal 4776 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 a2K(3rHj$^VY%~U(889)lFep3k9Q`kU29J{f literal 0 HcmV?d00001 diff --git a/apps/meteor/public/images/featurePreview/timestamp.png b/apps/meteor/public/images/featurePreview/timestamp.png new file mode 100644 index 0000000000000000000000000000000000000000..7573f97db55b73e2d880af67e6705e3c4a2040c7 GIT binary patch literal 51432 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

        &c?(AAUaFYNC38B5NhzYmJ_RR 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 p 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 i18nLabel: 'Sidebar_Sections_Order', i18nDescription: 'Sidebar_Sections_Order_Description', }); + + await this.add('Accounts_Default_User_Preferences_featuresPreview', '[]', { + type: 'string', + public: true, + }); }); await this.section('Avatar', async function () { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index b8341f7c0994..d933f1f3c4b3 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -186,6 +186,7 @@ describe('miscellaneous', () => { 'muteFocusedConversations', 'notifyCalendarEvents', 'enableMobileRinging', + 'featuresPreview', ].filter((p) => Boolean(p)); expect(res.body).to.have.property('success', true); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 8ce6bea2e117..27978d72e7dd 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -85,6 +85,7 @@ "Accounts_AllowEmailChange": "Allow Email Change", "Accounts_AllowEmailNotifications": "Allow Email Notifications", "Accounts_AllowFeaturePreview": "Allow Feature Preview", + "Accounts_AllowFeaturePreview_Description": "Make feature preview available to all workspace members.", "Accounts_AllowPasswordChange": "Allow Password Change", "Accounts_AllowPasswordChangeForOAuthUsers": "Allow Password Change for OAuth Users", "Accounts_AllowRealNameChange": "Allow Name Change", @@ -1125,8 +1126,8 @@ "Common_Access": "Common Access", "Commit": "Commit", "Community": "Community", - "Contextualbar_resizable": "Contextual bar resizable", - "Contextualbar_resizable_description": "Allows you to adjust the size of the contextual bar by simply dragging, giving you instant customization and flexibility", + "Contextualbar_resizable": "Resizable contextual bar", + "Contextualbar_resizable_description": "Adjust the size of the contextual bar by clicking and dragging the edge, giving you instant customization and flexibility.", "Free_Edition": "Free edition", "Composer_not_available_phone_calls": "Messages are not available on phone calls", "Condensed": "Condensed", @@ -1949,8 +1950,8 @@ "Enable_Password_History": "Enable Password History", "Enable_Password_History_Description": "When enabled, users won't be able to update their passwords to some of their most recently used passwords.", "Enable_Svg_Favicon": "Enable SVG favicon", - "Enable_timestamp": "Enable timestamp parsing in messages", - "Enable_timestamp_description": "Enable timestamps to be parsed in messages", + "Enable_timestamp": "Timestamp in messages", + "Enable_timestamp_description": "Render Unix timestamps inside messages in your local (system) timezone.", "Enable_to_bypass_email_verification": "Enable to bypass email verification", "Enable_two-factor_authentication": "Enable two-factor authentication via TOTP", "Enable_two-factor_authentication_email": "Enable two-factor authentication via Email", @@ -2284,7 +2285,10 @@ "Favorite_Rooms": "Enable Favorite Rooms", "Favorites": "Favorites", "Feature_preview": "Feature preview", - "Feature_preview_page_description": "Welcome to the features preview page! Here, you can enable the latest cutting-edge features that are currently under development and not yet officially released.\n\nPlease note that these configurations are still in the testing phase and may not be stable or fully functional.", + "Feature_preview_page_description": "Enable the latest features that are currently under development.", + "Feature_preview_page_callout": "Feature previews are being tested and may not be stable or fully functional. Features may become premium capabilities once officially released.", + "Feature_preview_admin_page_description": "Choose what feature previews to make available to workspace members.", + "Feature_preview_admin_page_callout": "Features enabled here will be enabled to each user in their feature preview preferences.", "featured": "featured", "Featured": "Featured", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "This feature depends on the above selected call provider to be enabled from the administration settings (Admin -> Video Conference).", @@ -4363,7 +4367,7 @@ "Queue_Time": "Queue Time", "Queue_management": "Queue Management", "Quick_reactions": "Quick reactions", - "Quick_reactions_description": "The three most used reactions get an easy access while your mouse is over the message", + "Quick_reactions_description": "Easily access your most used and most recent emoji message reactions by hovering on a message.", "quote": "quote", "Quote": "Quote", "Random": "Random", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 090e081e83fa..3110d6e82a67 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -2169,7 +2169,6 @@ "Favorite_Rooms": "पसंदीदा कमरे सक्षम करें", "Favorites": "पसंदीदा", "Feature_preview": "फ़ीचर पूर्वावलोकन", - "Feature_preview_page_description": "फीचर पूर्वावलोकन पृष्ठ पर आपका स्वागत है! यहां, आप नवीनतम अत्याधुनिक सुविधाओं को सक्षम कर सकते हैं जो वर्तमान में विकास के अधीन हैं और अभी तक आधिकारिक तौर पर जारी नहीं की गई हैं।\n\nकृपया ध्यान दें कि ये कॉन्फ़िगरेशन अभी भी परीक्षण चरण में हैं और स्थिर या पूरी तरह कार्यात्मक नहीं हो सकते हैं।", "featured": "प्रदर्शित", "Featured": "प्रदर्शित", "Feature_depends_on_selected_call_provider_to_be_enabled_from_administration_settings": "यह सुविधा प्रशासन सेटिंग्स (एडमिन -> वीडियो कॉन्फ्रेंस) से सक्षम होने के लिए उपरोक्त चयनित कॉल प्रदाता पर निर्भर करती है।", @@ -4128,7 +4127,6 @@ "Queue_Time": "कतार समय", "Queue_management": "कतार प्रबंधन", "Quick_reactions": "त्वरित प्रतिक्रियाएँ", - "Quick_reactions_description": "जब आपका माउस संदेश पर होता है तो सबसे अधिक उपयोग की जाने वाली तीन प्रतिक्रियाओं तक आसान पहुंच मिलती है", "quote": "उद्धरण", "Quote": "उद्धरण", "Random": "Random", @@ -4987,7 +4985,7 @@ "The_application_will_be_able_to": "<1>{{appName}} यह करने में सक्षम होगा:", "The_channel_name_is_required": "चैनल का नाम आवश्यक है", "The_emails_are_being_sent": "ईमेल भेजे जा रहे हैं.", - "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", + "The_empty_room__roomName__will_be_removed_automatically": "खाली कमरा {{roomName}} स्वचालित रूप से हटा दिया जाएगा।", "The_image_resize_will_not_work_because_we_can_not_detect_ImageMagick_or_GraphicsMagick_installed_in_your_server": "छवि का आकार बदलना काम नहीं करेगा क्योंकि हम आपके सर्वर पर स्थापित ImageMagick या ग्राफ़िक्सMagick का पता नहीं लगा सकते हैं।", "The_message_is_a_discussion_you_will_not_be_able_to_recover": "संदेश एक चर्चा है आप संदेशों को पुनर्प्राप्त नहीं कर पाएंगे!", "The_mobile_notifications_were_disabled_to_all_users_go_to_Admin_Push_to_enable_the_Push_Gateway_again": "मोबाइल सूचनाएं सभी उपयोगकर्ताओं के लिए अक्षम कर दी गई थीं, पुश गेटवे को फिर से सक्षम करने के लिए \"एडमिन > पुश\" पर जाएं", @@ -6134,4 +6132,4 @@ "Unlimited_seats": "असीमित सीटें", "Unlimited_MACs": "असीमित एमएसी", "Unlimited_seats_MACs": "असीमित सीटें और एमएसी" -} \ No newline at end of file +} diff --git a/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx new file mode 100644 index 000000000000..eece30cc7280 --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/FeaturePreviewBadge.tsx @@ -0,0 +1,21 @@ +import { Badge } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; + +import { usePreferenceFeaturePreviewList } from '../../hooks/usePreferenceFeaturePreviewList'; + +const FeaturePreviewBadge = () => { + const t = useTranslation(); + const { unseenFeatures } = usePreferenceFeaturePreviewList(); + + if (!unseenFeatures) { + return null; + } + + return ( + + {unseenFeatures} + + ); +}; + +export default FeaturePreviewBadge; diff --git a/packages/ui-client/src/components/FeaturePreview/index.ts b/packages/ui-client/src/components/FeaturePreview/index.ts new file mode 100644 index 000000000000..f6b8e5f2071e --- /dev/null +++ b/packages/ui-client/src/components/FeaturePreview/index.ts @@ -0,0 +1,2 @@ +export { FeaturePreview, FeaturePreviewOn, FeaturePreviewOff } from './FeaturePreview'; +export { default as FeaturePreviewBadge } from './FeaturePreviewBadge'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 8642983229aa..7308c8e75431 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -11,7 +11,7 @@ export * as UserStatus from './UserStatus'; export * from './Header'; export * from './HeaderV2'; export * from './MultiSelectCustom/MultiSelectCustom'; -export * from './FeaturePreview/FeaturePreview'; +export * from './FeaturePreview'; export * from './RoomBanner'; export { default as UserAutoComplete } from './UserAutoComplete'; export * from './GenericMenu'; diff --git a/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts new file mode 100644 index 000000000000..373862379cc1 --- /dev/null +++ b/packages/ui-client/src/hooks/useDefaultSettingFeaturePreviewList.ts @@ -0,0 +1,12 @@ +import { useSetting } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const useDefaultSettingFeaturePreviewList = () => { + const featurePreviewSettingJSON = useSetting('Accounts_Default_User_Preferences_featuresPreview'); + + const settingFeaturePreview = useMemo(() => parseSetting(featurePreviewSettingJSON), [featurePreviewSettingJSON]); + + return useFeaturePreviewList(settingFeaturePreview ?? []); +}; diff --git a/packages/ui-client/src/hooks/useFeaturePreview.ts b/packages/ui-client/src/hooks/useFeaturePreview.ts index 4bdda9c9251a..bd46adfdefff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreview.ts +++ b/packages/ui-client/src/hooks/useFeaturePreview.ts @@ -1,7 +1,8 @@ -import { type FeaturesAvailable, useFeaturePreviewList } from './useFeaturePreviewList'; +import { type FeaturesAvailable } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; export const useFeaturePreview = (featureName: FeaturesAvailable) => { - const { features } = useFeaturePreviewList(); + const { features } = usePreferenceFeaturePreviewList(); const currentFeature = features?.find((feature) => feature.name === featureName); diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.ts b/packages/ui-client/src/hooks/useFeaturePreviewList.ts index ff103a8d84ef..08bda4ff81ff 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.ts +++ b/packages/ui-client/src/hooks/useFeaturePreviewList.ts @@ -1,5 +1,4 @@ import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts'; export type FeaturesAvailable = | 'quickReactions' @@ -24,6 +23,7 @@ export type FeaturePreviewProps = { }; }; +// TODO: Move the features preview array to another directory to be accessed from both BE and FE. export const defaultFeaturesPreview: FeaturePreviewProps[] = [ { name: 'quickReactions', @@ -47,6 +47,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Enable_timestamp', description: 'Enable_timestamp_description', group: 'Message', + imageUrl: 'images/featurePreview/timestamp.png', value: false, enabled: true, }, @@ -55,6 +56,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'Contextualbar_resizable', description: 'Contextualbar_resizable_description', group: 'Navigation', + imageUrl: 'images/featurePreview/resizable-contextual-bar.png', value: false, enabled: true, }, @@ -63,6 +65,7 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ i18n: 'New_navigation', description: 'New_navigation_description', group: 'Navigation', + imageUrl: 'images/featurePreview/enhanced-navigation.png', value: false, enabled: true, }, @@ -82,22 +85,27 @@ export const defaultFeaturesPreview: FeaturePreviewProps[] = [ export const enabledDefaultFeatures = defaultFeaturesPreview.filter((feature) => feature.enabled); -export const useFeaturePreviewList = () => { - const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); - const userFeaturesPreview = useUserPreference('featuresPreview'); - - if (!featurePreviewEnabled) { - return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; +// TODO: Remove this logic after we have a way to store object settings. +export const parseSetting = (setting?: FeaturePreviewProps[] | string) => { + if (typeof setting === 'string') { + try { + return JSON.parse(setting) as FeaturePreviewProps[]; + } catch (_) { + return; + } } + return setting; +}; +export const useFeaturePreviewList = (featuresList: Pick[]) => { const unseenFeatures = enabledDefaultFeatures.filter( - (feature) => !userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name), + (defaultFeature) => !featuresList?.find((feature) => feature.name === defaultFeature.name), ).length; - const mergedFeatures = enabledDefaultFeatures.map((feature) => { - const userFeature = userFeaturesPreview?.find((userFeature) => userFeature.name === feature.name); - return { ...feature, ...userFeature }; + const mergedFeatures = enabledDefaultFeatures.map((defaultFeature) => { + const features = featuresList?.find((feature) => feature.name === defaultFeature.name); + return { ...defaultFeature, ...features }; }); - return { unseenFeatures, features: mergedFeatures, featurePreviewEnabled }; + return { unseenFeatures, features: mergedFeatures }; }; diff --git a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx similarity index 79% rename from packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx rename to packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx index e348cfb6a864..ac3d6f92d51a 100644 --- a/packages/ui-client/src/hooks/useFeaturePreviewList.spec.tsx +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.spec.tsx @@ -1,10 +1,11 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { renderHook } from '@testing-library/react'; -import { useFeaturePreviewList, enabledDefaultFeatures } from './useFeaturePreviewList'; +import { enabledDefaultFeatures } from './useFeaturePreviewList'; +import { usePreferenceFeaturePreviewList } from './usePreferenceFeaturePreviewList'; it('should return the number of unseen features and Accounts_AllowFeaturePreview enabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', true).build(), }); @@ -18,7 +19,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return the number of unseen features and Accounts_AllowFeaturePreview disabled ', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot().withSetting('Accounts_AllowFeaturePreview', false).build(), }); @@ -32,7 +33,7 @@ it('should return the number of unseen features and Accounts_AllowFeaturePreview }); it('should return 0 unseen features', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -49,7 +50,7 @@ it('should return 0 unseen features', () => { }); it('should ignore removed feature previews', () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) @@ -72,7 +73,7 @@ it('should ignore removed feature previews', () => { }); it('should turn off ignored feature previews', async () => { - const { result } = renderHook(() => useFeaturePreviewList(), { + const { result } = renderHook(() => usePreferenceFeaturePreviewList(), { legacyRoot: true, wrapper: mockAppRoot() .withSetting('Accounts_AllowFeaturePreview', true) diff --git a/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts new file mode 100644 index 000000000000..d7c4c13417d2 --- /dev/null +++ b/packages/ui-client/src/hooks/usePreferenceFeaturePreviewList.ts @@ -0,0 +1,16 @@ +import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; + +import { FeaturePreviewProps, parseSetting, useFeaturePreviewList } from './useFeaturePreviewList'; + +export const usePreferenceFeaturePreviewList = () => { + const featurePreviewEnabled = useSetting('Accounts_AllowFeaturePreview'); + const userFeaturesPreviewPreference = useUserPreference('featuresPreview'); + const userFeaturesPreview = useMemo(() => parseSetting(userFeaturesPreviewPreference), [userFeaturesPreviewPreference]); + const { unseenFeatures, features } = useFeaturePreviewList(userFeaturesPreview ?? []); + + if (!featurePreviewEnabled) { + return { unseenFeatures: 0, features: [] as FeaturePreviewProps[], featurePreviewEnabled }; + } + return { unseenFeatures, features, featurePreviewEnabled }; +}; diff --git a/packages/ui-client/src/index.ts b/packages/ui-client/src/index.ts index 3e640343da5b..a96ef265aadc 100644 --- a/packages/ui-client/src/index.ts +++ b/packages/ui-client/src/index.ts @@ -1,5 +1,7 @@ export * from './components'; export * from './hooks/useFeaturePreview'; +export * from './hooks/useDefaultSettingFeaturePreviewList'; export * from './hooks/useFeaturePreviewList'; +export * from './hooks/usePreferenceFeaturePreviewList'; export * from './hooks/useDocumentTitle'; export * from './helpers'; From 519ed86b392219851c28b101b1219c22caf23c9e Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 24 Sep 2024 12:03:20 -0600 Subject: [PATCH 43/57] fix: Avoid destructuring `connectionData` when value is undefined (#33339) --- .changeset/brave-brooms-invent.md | 5 ++++ .../app/livechat/server/lib/LivechatTyped.ts | 8 ++++-- .../end-to-end/api/livechat/09-visitors.ts | 28 +++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 .changeset/brave-brooms-invent.md diff --git a/.changeset/brave-brooms-invent.md b/.changeset/brave-brooms-invent.md new file mode 100644 index 000000000000..35d32b485944 --- /dev/null +++ b/.changeset/brave-brooms-invent.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a problem that caused visitor creation to fail when GDPR setting was enabled and visitor was created via Apps Engine or the deprecated `livechat:registerGuest` method. diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index ade6726336ec..6c2d655f4c95 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -589,6 +589,10 @@ class LivechatClass { } } + isValidObject(obj: unknown): obj is Record { + return typeof obj === 'object' && obj !== null; + } + async registerGuest({ id, token, @@ -654,10 +658,10 @@ class LivechatClass { visitorDataToUpdate.status = status; visitorDataToUpdate.ts = new Date(); - if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) { + if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations') && Livechat.isValidObject(connectionData)) { Livechat.logger.debug(`Saving connection data for visitor ${token}`); const { httpHeaders, clientAddress } = connectionData; - if (httpHeaders) { + if (Livechat.isValidObject(httpHeaders)) { visitorDataToUpdate.userAgent = httpHeaders['user-agent']; visitorDataToUpdate.ip = httpHeaders['x-real-ip'] || httpHeaders['x-forwarded-for'] || clientAddress; visitorDataToUpdate.host = httpHeaders?.host; diff --git a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts index f02d9d1d1e95..31134412b8ec 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/09-visitors.ts @@ -1,11 +1,11 @@ import { faker } from '@faker-js/faker'; import type { ILivechatVisitor } from '@rocket.chat/core-typings'; import { expect } from 'chai'; -import { before, describe, it } from 'mocha'; +import { before, describe, it, after } from 'mocha'; import moment from 'moment'; import { type Response } from 'supertest'; -import { getCredentials, api, request, credentials } from '../../../data/api-data'; +import { getCredentials, api, request, credentials, methodCallAnon } from '../../../data/api-data'; import { createCustomField, deleteCustomField } from '../../../data/livechat/custom-fields'; import { makeAgentAvailable, @@ -217,6 +217,30 @@ describe('LIVECHAT - visitors', () => { expect(body.visitor).to.have.property('livechatData'); expect(body.visitor.livechatData).to.have.property(customFieldName, 'Not a real address :)'); }); + + describe('special cases', () => { + before(async () => { + await updateSetting('Livechat_Allow_collect_and_store_HTTP_header_informations', true); + }); + after(async () => { + await updateSetting('Livechat_Allow_collect_and_store_HTTP_header_informations', false); + }); + + // Note: this had to use the meteor method because the endpoint used `req.headers` which we cannot send as empty + // method doesn't pass them to the func allowing us to create a test for it + it('should allow to create a visitor without passing connectionData when GDPR setting is enabled', async () => { + const token = `${new Date().getTime()}-test`; + const response = await request + .post(methodCallAnon('livechat:registerGuest')) + .send({ message: `{"msg":"method","id":"23","method":"livechat:registerGuest","params":[{ "token": "${token}"}]}` }); + + expect(response.body).to.have.property('success', true); + const r = JSON.parse(response.body.message); + + expect(r.result).to.have.property('visitor'); + expect(r.result.visitor).to.have.property('token', token); + }); + }); }); describe('livechat/visitors.info', () => { From 2327ff58b2b06f127ac2d9fbc5b37ceb7202f752 Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 24 Sep 2024 21:16:36 +0000 Subject: [PATCH 44/57] Release 6.13.0-rc.1 [no ci] --- .changeset/bump-patch-1727212585363.md | 5 +++ .changeset/pre.json | 3 ++ apps/meteor/CHANGELOG.md | 39 +++++++++++++++++++ apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/ee/server/services/CHANGELOG.md | 14 +++++++ apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- apps/uikit-playground/CHANGELOG.md | 13 +++++++ apps/uikit-playground/package.json | 2 +- ee/apps/account-service/CHANGELOG.md | 14 +++++++ ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/CHANGELOG.md | 14 +++++++ ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/CHANGELOG.md | 15 +++++++ ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/CHANGELOG.md | 15 +++++++ ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/CHANGELOG.md | 14 +++++++ ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/CHANGELOG.md | 14 +++++++ ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/CHANGELOG.md | 12 ++++++ ee/apps/stream-hub-service/package.json | 2 +- ee/packages/license/CHANGELOG.md | 10 +++++ ee/packages/license/package.json | 2 +- ee/packages/omnichannel-services/CHANGELOG.md | 15 +++++++ ee/packages/omnichannel-services/package.json | 2 +- ee/packages/pdf-worker/CHANGELOG.md | 10 +++++ ee/packages/pdf-worker/package.json | 2 +- ee/packages/presence/CHANGELOG.md | 12 ++++++ ee/packages/presence/package.json | 2 +- package.json | 2 +- packages/api-client/CHANGELOG.md | 11 ++++++ packages/api-client/package.json | 2 +- packages/apps/CHANGELOG.md | 11 ++++++ packages/apps/package.json | 2 +- packages/core-services/CHANGELOG.md | 12 ++++++ packages/core-services/package.json | 2 +- packages/core-typings/CHANGELOG.md | 3 ++ packages/core-typings/package.json | 2 +- packages/cron/CHANGELOG.md | 11 ++++++ packages/cron/package.json | 2 +- packages/ddp-client/CHANGELOG.md | 12 ++++++ packages/ddp-client/package.json | 2 +- packages/fuselage-ui-kit/CHANGELOG.md | 14 +++++++ packages/fuselage-ui-kit/package.json | 8 ++-- packages/gazzodown/CHANGELOG.md | 12 ++++++ packages/gazzodown/package.json | 6 +-- packages/i18n/CHANGELOG.md | 6 +++ packages/i18n/package.json | 2 +- packages/instance-status/CHANGELOG.md | 10 +++++ packages/instance-status/package.json | 2 +- packages/livechat/CHANGELOG.md | 10 +++++ packages/livechat/package.json | 2 +- packages/mock-providers/CHANGELOG.md | 9 +++++ packages/mock-providers/package.json | 2 +- packages/model-typings/CHANGELOG.md | 10 +++++ packages/model-typings/package.json | 2 +- packages/models/CHANGELOG.md | 10 +++++ packages/models/package.json | 2 +- packages/rest-typings/CHANGELOG.md | 10 +++++ packages/rest-typings/package.json | 2 +- packages/ui-avatar/CHANGELOG.md | 9 +++++ packages/ui-avatar/package.json | 4 +- packages/ui-client/CHANGELOG.md | 15 +++++++ packages/ui-client/package.json | 6 +-- packages/ui-contexts/CHANGELOG.md | 13 +++++++ packages/ui-contexts/package.json | 2 +- packages/ui-video-conf/CHANGELOG.md | 11 ++++++ packages/ui-video-conf/package.json | 6 +-- packages/web-ui-registration/CHANGELOG.md | 9 +++++ packages/web-ui-registration/package.json | 4 +- 72 files changed, 474 insertions(+), 47 deletions(-) create mode 100644 .changeset/bump-patch-1727212585363.md diff --git a/.changeset/bump-patch-1727212585363.md b/.changeset/bump-patch-1727212585363.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1727212585363.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/pre.json b/.changeset/pre.json index 6976ce2b60aa..7c415a6b0dde 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -63,7 +63,9 @@ "@rocket.chat/web-ui-registration": "10.0.0" }, "changesets": [ + "brave-brooms-invent", "brown-singers-appear", + "bump-patch-1727212585363", "cyan-ladybugs-thank", "dirty-stingrays-beg", "five-coats-rhyme", @@ -80,6 +82,7 @@ "mighty-drinks-hide", "nasty-tools-enjoy", "pink-swans-teach", + "quick-rings-wave", "quiet-cherries-punch", "rich-toes-bow", "rotten-rabbits-brush", diff --git a/apps/meteor/CHANGELOG.md b/apps/meteor/CHANGELOG.md index ee5d553d85a5..c31645ca4ea4 100644 --- a/apps/meteor/CHANGELOG.md +++ b/apps/meteor/CHANGELOG.md @@ -1,5 +1,44 @@ # @rocket.chat/meteor +## 6.13.0-rc.1 + +### Minor Changes + +- ([#33212](https://github.com/RocketChat/Rocket.Chat/pull/33212)) Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. + +### Patch Changes + +- ([#33339](https://github.com/RocketChat/Rocket.Chat/pull/33339)) Fixes a problem that caused visitor creation to fail when GDPR setting was enabled and visitor was created via Apps Engine or the deprecated `livechat:registerGuest` method. + +- Bump @rocket.chat/meteor version. + +-

        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/i18n@0.8.0-rc.1 + - @rocket.chat/ui-client@11.0.0-rc.1 + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/web-ui-registration@11.0.0-rc.1 + - @rocket.chat/gazzodown@11.0.0-rc.1 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.1 + - @rocket.chat/ui-theming@0.3.0-rc.0 + - @rocket.chat/ui-avatar@7.0.0-rc.1 + - @rocket.chat/ui-video-conf@11.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/license@0.2.8-rc.1 + - @rocket.chat/omnichannel-services@0.3.5-rc.1 + - @rocket.chat/pdf-worker@0.2.5-rc.1 + - @rocket.chat/presence@0.2.8-rc.1 + - @rocket.chat/api-client@0.2.8-rc.1 + - @rocket.chat/apps@0.1.8-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/cron@0.1.8-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/server-cloud-communication@0.0.2 + - @rocket.chat/models@0.3.0-rc.1 + - @rocket.chat/instance-status@0.1.8-rc.1 +
        + ## 6.13.0-rc.0 ### Minor Changes diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index fc41ef05cacd..cfec7fa33824 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-rc.0" + "version": "6.13.0-rc.1" } diff --git a/apps/meteor/ee/server/services/CHANGELOG.md b/apps/meteor/ee/server/services/CHANGELOG.md index fb0072738db2..59da7bfded9c 100644 --- a/apps/meteor/ee/server/services/CHANGELOG.md +++ b/apps/meteor/ee/server/services/CHANGELOG.md @@ -1,5 +1,18 @@ # rocketchat-services +## 1.3.5-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 1.3.5-rc.0 ### Patch Changes @@ -13,6 +26,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 1.3.4 ### Patch Changes diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 2cab26ca2d38..540a283be188 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -1,7 +1,7 @@ { "name": "rocketchat-services", "private": true, - "version": "1.3.5-rc.0", + "version": "1.3.5-rc.1", "description": "Rocket.Chat Authorization service", "main": "index.js", "scripts": { diff --git a/apps/meteor/package.json b/apps/meteor/package.json index a30ce30a7b6f..a36621eac547 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "private": true, "author": { "name": "Rocket.Chat", diff --git a/apps/uikit-playground/CHANGELOG.md b/apps/uikit-playground/CHANGELOG.md index d1528ca7502d..d2793509ff54 100644 --- a/apps/uikit-playground/CHANGELOG.md +++ b/apps/uikit-playground/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/uikit-playground +## 0.5.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/fuselage-ui-kit@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.5.0-rc.0 ### Minor Changes @@ -15,6 +27,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 0.4.1 ### Patch Changes diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index c5e7628001c6..82ebd11ee8dc 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/uikit-playground", "private": true, - "version": "0.5.0-rc.0", + "version": "0.5.0-rc.1", "type": "module", "scripts": { "dev": "vite", diff --git a/ee/apps/account-service/CHANGELOG.md b/ee/apps/account-service/CHANGELOG.md index 929082c7bb9a..4c86f21b24c4 100644 --- a/ee/apps/account-service/CHANGELOG.md +++ b/ee/apps/account-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/account-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/core-services@0.7.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index f2c7dd0a8761..36ea1d9294c6 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/account-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Account service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/authorization-service/CHANGELOG.md b/ee/apps/authorization-service/CHANGELOG.md index 5be055a875d0..c587761cdb38 100644 --- a/ee/apps/authorization-service/CHANGELOG.md +++ b/ee/apps/authorization-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/authorization-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/core-services@0.7.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index eaa51c88691c..255b76d2db2e 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/authorization-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Authorization service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/ddp-streamer/CHANGELOG.md b/ee/apps/ddp-streamer/CHANGELOG.md index 4b61203020bf..975b9af1abf2 100644 --- a/ee/apps/ddp-streamer/CHANGELOG.md +++ b/ee/apps/ddp-streamer/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ddp-streamer +## 0.3.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 + - @rocket.chat/instance-status@0.1.8-rc.1 +
        + ## 0.3.8-rc.0 ### Patch Changes @@ -13,6 +27,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/instance-status@0.1.7-rc.0 + ## 0.3.7 ### Patch Changes diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 16dcc7b6f507..7fd25289ca55 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/ddp-streamer", "private": true, - "version": "0.3.8-rc.0", + "version": "0.3.8-rc.1", "description": "Rocket.Chat DDP-Streamer service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/omnichannel-transcript/CHANGELOG.md b/ee/apps/omnichannel-transcript/CHANGELOG.md index b934e4037e8a..613d6ef3f377 100644 --- a/ee/apps/omnichannel-transcript/CHANGELOG.md +++ b/ee/apps/omnichannel-transcript/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-transcript +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.5-rc.1 + - @rocket.chat/pdf-worker@0.2.5-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -13,6 +27,7 @@ - @rocket.chat/omnichannel-services@0.3.4-rc.0 - @rocket.chat/pdf-worker@0.2.4-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 2f83816f4650..b0f97ce6d562 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/omnichannel-transcript", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/presence-service/CHANGELOG.md b/ee/apps/presence-service/CHANGELOG.md index 78b716888236..795b6a2df283 100644 --- a/ee/apps/presence-service/CHANGELOG.md +++ b/ee/apps/presence-service/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/presence-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/presence@0.2.8-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/presence@0.2.7-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index 7c21dbbdcbec..d1eb50267c52 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/presence-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Presence service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/queue-worker/CHANGELOG.md b/ee/apps/queue-worker/CHANGELOG.md index 9ac7c4bed05f..cca63889589c 100644 --- a/ee/apps/queue-worker/CHANGELOG.md +++ b/ee/apps/queue-worker/CHANGELOG.md @@ -1,5 +1,18 @@ # @rocket.chat/queue-worker +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/omnichannel-services@0.3.5-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes @@ -12,6 +25,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/omnichannel-services@0.3.4-rc.0 + ## 0.4.7 ### Patch Changes diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index b800469a8274..634d49159ea8 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/queue-worker", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/apps/stream-hub-service/CHANGELOG.md b/ee/apps/stream-hub-service/CHANGELOG.md index 550a6a251252..05843d5502b1 100644 --- a/ee/apps/stream-hub-service/CHANGELOG.md +++ b/ee/apps/stream-hub-service/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/stream-hub-service +## 0.4.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.4.8-rc.0 ### Patch Changes diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 1ffac9a8775c..dca9ad443b3d 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/stream-hub-service", "private": true, - "version": "0.4.8-rc.0", + "version": "0.4.8-rc.1", "description": "Rocket.Chat Stream Hub service", "scripts": { "build": "tsc -p tsconfig.json", diff --git a/ee/packages/license/CHANGELOG.md b/ee/packages/license/CHANGELOG.md index 05c4edfb5e77..92b07995bb4b 100644 --- a/ee/packages/license/CHANGELOG.md +++ b/ee/packages/license/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/license +## 0.2.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.2.8-rc.0 ### Patch Changes @@ -8,6 +17,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.2.7 ### Patch Changes diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 24b5676b8fc0..30a89b4334bd 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/license", - "version": "0.2.8-rc.0", + "version": "0.2.8-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/omnichannel-services/CHANGELOG.md b/ee/packages/omnichannel-services/CHANGELOG.md index 3cfb53628c15..339277317d7f 100644 --- a/ee/packages/omnichannel-services/CHANGELOG.md +++ b/ee/packages/omnichannel-services/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/omnichannel-services +## 0.3.5-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/pdf-worker@0.2.5-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.3.5-rc.0 ### Patch Changes @@ -13,6 +27,7 @@ - @rocket.chat/models@0.3.0-rc.0 - @rocket.chat/pdf-worker@0.2.4-rc.0 + ## 0.3.4 ### Patch Changes diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 227ac2927974..98f507325586 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/omnichannel-services", - "version": "0.3.5-rc.0", + "version": "0.3.5-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/ee/packages/pdf-worker/CHANGELOG.md b/ee/packages/pdf-worker/CHANGELOG.md index 785607cc6856..60f03a15a1de 100644 --- a/ee/packages/pdf-worker/CHANGELOG.md +++ b/ee/packages/pdf-worker/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/pdf-worker +## 0.2.5-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.2.5-rc.0 ### Patch Changes @@ -8,6 +17,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.2.4 ### Patch Changes diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index dbc1f4483d23..570b3baab2e2 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/pdf-worker", - "version": "0.2.5-rc.0", + "version": "0.2.5-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/ee/packages/presence/CHANGELOG.md b/ee/packages/presence/CHANGELOG.md index 428dcbca5933..7b8bc5b28116 100644 --- a/ee/packages/presence/CHANGELOG.md +++ b/ee/packages/presence/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/presence +## 0.2.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/core-services@0.7.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.2.8-rc.0 ### Patch Changes @@ -10,6 +21,7 @@ - @rocket.chat/core-services@0.7.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.2.7 ### Patch Changes diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index a0a9d059dead..912b4bf453fd 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/presence", - "version": "0.2.8-rc.0", + "version": "0.2.8-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/package.json b/package.json index 2f89cab55d2a..1a87fa6034bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/api-client/CHANGELOG.md b/packages/api-client/CHANGELOG.md index 8158e1f876d5..18b99f365728 100644 --- a/packages/api-client/CHANGELOG.md +++ b/packages/api-client/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/api-client +## 0.2.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 +
        + ## 0.2.8-rc.0 ### Patch Changes @@ -9,6 +19,7 @@ - @rocket.chat/rest-typings@6.13.0-rc.0 - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.2.7 ### Patch Changes diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 10492ffedab4..b87cdb6600d9 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/api-client", - "version": "0.2.8-rc.0", + "version": "0.2.8-rc.1", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/apps/CHANGELOG.md b/packages/apps/CHANGELOG.md index 10681302f29e..fcf1229c306a 100644 --- a/packages/apps/CHANGELOG.md +++ b/packages/apps/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/apps +## 0.1.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/model-typings@0.8.0-rc.1 +
        + ## 0.1.8-rc.0 ### Patch Changes @@ -9,6 +19,7 @@ - @rocket.chat/model-typings@0.8.0-rc.0 - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/apps/package.json b/packages/apps/package.json index 7dc954e5c3db..af88aff27664 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps", - "version": "0.1.8-rc.0", + "version": "0.1.8-rc.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/core-services/CHANGELOG.md b/packages/core-services/CHANGELOG.md index b1c5158d51a4..aafc8e1f0c6c 100644 --- a/packages/core-services/CHANGELOG.md +++ b/packages/core-services/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/core-services +## 0.7.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.7.0-rc.0 ### Minor Changes @@ -19,6 +30,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.6.1 ### Patch Changes diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 7326f3cd3617..2ee07033e732 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/core-services", - "version": "0.7.0-rc.0", + "version": "0.7.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", diff --git a/packages/core-typings/CHANGELOG.md b/packages/core-typings/CHANGELOG.md index 4d686538954f..e7171dedd7b6 100644 --- a/packages/core-typings/CHANGELOG.md +++ b/packages/core-typings/CHANGELOG.md @@ -1,5 +1,7 @@ # @rocket.chat/core-typings +## 6.13.0-rc.1 + ## 6.13.0-rc.0 ### Minor Changes @@ -16,6 +18,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 + ## 6.12.1 ### Patch Changes diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 2c5cb3f64a2d..6fcd5a43d20d 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/cron/CHANGELOG.md b/packages/cron/CHANGELOG.md index 27aa9beb2b65..297acb29b0cf 100644 --- a/packages/cron/CHANGELOG.md +++ b/packages/cron/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/cron +## 0.1.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.1.8-rc.0 ### Patch Changes @@ -9,6 +19,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/models@0.3.0-rc.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/cron/package.json b/packages/cron/package.json index 798e07348d3b..f9ac01ce7740 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/cron", - "version": "0.1.8-rc.0", + "version": "0.1.8-rc.1", "private": true, "devDependencies": { "eslint": "~8.45.0", diff --git a/packages/ddp-client/CHANGELOG.md b/packages/ddp-client/CHANGELOG.md index ac4b3f76b11c..5e81a05f3804 100644 --- a/packages/ddp-client/CHANGELOG.md +++ b/packages/ddp-client/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/ddp-client +## 0.3.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/api-client@0.2.8-rc.1 +
        + ## 0.3.8-rc.0 ### Patch Changes @@ -10,6 +21,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/api-client@0.2.7-rc.0 + ## 0.3.7 ### Patch Changes diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 949fe4eebd65..c2dc8b95a4b9 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ddp-client", - "version": "0.3.8-rc.0", + "version": "0.3.8-rc.1", "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/fuselage-ui-kit/CHANGELOG.md b/packages/fuselage-ui-kit/CHANGELOG.md index 895ffaf94ab4..14c2fea3c3c1 100644 --- a/packages/fuselage-ui-kit/CHANGELOG.md +++ b/packages/fuselage-ui-kit/CHANGELOG.md @@ -1,5 +1,18 @@ # Change Log +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/gazzodown@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 + - @rocket.chat/ui-video-conf@11.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -20,6 +33,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 573d65dc215e..975051db933f 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/fuselage-ui-kit", "private": true, - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "description": "UiKit elements for Rocket.Chat Apps built under Fuselage design system", "homepage": "https://rocketchat.github.io/Rocket.Chat.Fuselage/", "author": { @@ -50,10 +50,10 @@ "@rocket.chat/icons": "*", "@rocket.chat/prettier-config": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "7.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-avatar": "7.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "@rocket.chat/ui-kit": "*", - "@rocket.chat/ui-video-conf": "11.0.0-rc.0", + "@rocket.chat/ui-video-conf": "11.0.0-rc.1", "@tanstack/react-query": "*", "react": "*", "react-dom": "*" diff --git a/packages/gazzodown/CHANGELOG.md b/packages/gazzodown/CHANGELOG.md index 9c4e6777a8a8..5ce5e8e7afed 100644 --- a/packages/gazzodown/CHANGELOG.md +++ b/packages/gazzodown/CHANGELOG.md @@ -1,5 +1,16 @@ # @rocket.chat/gazzodown +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/ui-client@11.0.0-rc.1 + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -15,6 +26,7 @@ - @rocket.chat/message-parser@0.31.30-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index b9a5023ce13c..cf4a9209d649 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/gazzodown", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -74,8 +74,8 @@ "@rocket.chat/fuselage-tokens": "*", "@rocket.chat/message-parser": "0.31.31-rc.0", "@rocket.chat/styled": "*", - "@rocket.chat/ui-client": "11.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-client": "11.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "katex": "*", "react": "*" }, diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 27bb4b471c66..3fa9140d4e99 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -1,5 +1,11 @@ # @rocket.chat/i18n +## 0.8.0-rc.1 + +### Minor Changes + +- ([#33212](https://github.com/RocketChat/Rocket.Chat/pull/33212)) Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. + ## 0.8.0-rc.0 ### Minor Changes diff --git a/packages/i18n/package.json b/packages/i18n/package.json index fe98e3b1f7fc..b93b9e9926cc 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/i18n", - "version": "0.8.0-rc.0", + "version": "0.8.0-rc.1", "private": true, "type": "module", "main": "./dist/index.js", diff --git a/packages/instance-status/CHANGELOG.md b/packages/instance-status/CHANGELOG.md index a2df17f053d2..dacdd5e1d383 100644 --- a/packages/instance-status/CHANGELOG.md +++ b/packages/instance-status/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/instance-status +## 0.1.8-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/models@0.3.0-rc.1 +
        + ## 0.1.8-rc.0 ### Patch Changes @@ -8,6 +17,7 @@ - @rocket.chat/models@0.3.0-rc.0 + ## 0.1.7 ### Patch Changes diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index a99c78a1ea78..79f0a19faf15 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/instance-status", - "version": "0.1.8-rc.0", + "version": "0.1.8-rc.1", "private": true, "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md index 480e9f27b435..b5e6fc40dff6 100644 --- a/packages/livechat/CHANGELOG.md +++ b/packages/livechat/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/livechat Change Log +## 1.20.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/gazzodown@11.0.0-rc.1 +
        + ## 1.20.0-rc.0 ### Minor Changes @@ -14,6 +23,7 @@ - @rocket.chat/gazzodown@11.0.0-rc.0 - @rocket.chat/message-parser@0.31.30-rc.0 + ## 1.19.4 ### Patch Changes diff --git a/packages/livechat/package.json b/packages/livechat/package.json index ab4a2a9b3a11..5663d8746eea 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/livechat", - "version": "1.20.0-rc.0", + "version": "1.20.0-rc.1", "files": [ "/build" ], diff --git a/packages/mock-providers/CHANGELOG.md b/packages/mock-providers/CHANGELOG.md index 5bd539fda340..a18b8b2e51fc 100644 --- a/packages/mock-providers/CHANGELOG.md +++ b/packages/mock-providers/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/mock-providers +## 0.1.3-rc.1 + +### Patch Changes + +-
        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/i18n@0.8.0-rc.1 +
        + ## 0.1.3-rc.0 ### Patch Changes diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 1a7051e87ff2..3cd4aef7325d 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mock-providers", - "version": "0.1.3-rc.0", + "version": "0.1.3-rc.1", "private": true, "dependencies": { "@rocket.chat/emitter": "~0.31.25", diff --git a/packages/model-typings/CHANGELOG.md b/packages/model-typings/CHANGELOG.md index ecec03565e40..54c1a0769d1e 100644 --- a/packages/model-typings/CHANGELOG.md +++ b/packages/model-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/model-typings +## 0.8.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 0.8.0-rc.0 ### Minor Changes @@ -18,6 +27,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 + ## 0.7.1 ### Patch Changes diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 10a2fd9b643c..2c535eb950ad 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/model-typings", - "version": "0.8.0-rc.0", + "version": "0.8.0-rc.1", "private": true, "devDependencies": { "@types/node-rsa": "^1.1.4", diff --git a/packages/models/CHANGELOG.md b/packages/models/CHANGELOG.md index 5491066cea23..a332d48b4443 100644 --- a/packages/models/CHANGELOG.md +++ b/packages/models/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/models +## 0.3.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/model-typings@0.8.0-rc.1 +
        + ## 0.3.0-rc.0 ### Minor Changes @@ -12,6 +21,7 @@ - @rocket.chat/model-typings@0.8.0-rc.0 + ## 0.2.4 ### Patch Changes diff --git a/packages/models/package.json b/packages/models/package.json index 35064171d9a6..aee39a8e841c 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/models", - "version": "0.3.0-rc.0", + "version": "0.3.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", diff --git a/packages/rest-typings/CHANGELOG.md b/packages/rest-typings/CHANGELOG.md index 8d999a8e7f5f..8dff96c33b7e 100644 --- a/packages/rest-typings/CHANGELOG.md +++ b/packages/rest-typings/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/rest-typings +## 6.13.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/core-typings@6.13.0-rc.1 +
        + ## 6.13.0-rc.0 ### Minor Changes @@ -23,6 +32,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/message-parser@0.31.30-rc.0 + ## 6.12.1 ### Patch Changes diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 33aad6d64823..6d435a93ccfc 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-rc.0", + "version": "6.13.0-rc.1", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/packages/ui-avatar/CHANGELOG.md b/packages/ui-avatar/CHANGELOG.md index 8192d99f770b..b5fc9c4b46d3 100644 --- a/packages/ui-avatar/CHANGELOG.md +++ b/packages/ui-avatar/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/ui-avatar +## 7.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 +
        + ## 7.0.0-rc.0 ### Minor Changes diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index 70f0efe2cff9..5c319c8837a5 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-avatar", - "version": "7.0.0-rc.0", + "version": "7.0.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -30,7 +30,7 @@ ], "peerDependencies": { "@rocket.chat/fuselage": "*", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "react": "~17.0.2" }, "volta": { diff --git a/packages/ui-client/CHANGELOG.md b/packages/ui-client/CHANGELOG.md index 9be8c33c299d..0d5f01b17910 100644 --- a/packages/ui-client/CHANGELOG.md +++ b/packages/ui-client/CHANGELOG.md @@ -1,5 +1,19 @@ # @rocket.chat/ui-client +## 11.0.0-rc.1 + +### Minor Changes + +- ([#33212](https://github.com/RocketChat/Rocket.Chat/pull/33212)) Added new Admin Feature Preview management view, this will allow the workspace admins to both enable feature previewing in the workspace as well as define which feature previews are enabled by default for the users in the workspace. + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -17,6 +31,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 40bccd8ca628..3b551cf83681 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-client", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", @@ -61,8 +61,8 @@ "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", - "@rocket.chat/ui-avatar": "7.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-avatar": "7.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "react": "*", "react-i18next": "*" }, diff --git a/packages/ui-contexts/CHANGELOG.md b/packages/ui-contexts/CHANGELOG.md index 9dcb89eda51d..4e29a0d34993 100644 --- a/packages/ui-contexts/CHANGELOG.md +++ b/packages/ui-contexts/CHANGELOG.md @@ -1,5 +1,17 @@ # @rocket.chat/ui-contexts +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies [2f9eea03d2]: + + - @rocket.chat/i18n@0.8.0-rc.1 + - @rocket.chat/core-typings@6.13.0-rc.1 + - @rocket.chat/rest-typings@6.13.0-rc.1 + - @rocket.chat/ddp-client@0.3.8-rc.1 +
        + ## 11.0.0-rc.0 ### Patch Changes @@ -11,6 +23,7 @@ - @rocket.chat/core-typings@6.13.0-rc.0 - @rocket.chat/ddp-client@0.3.7-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 37f83e96a2aa..c3b0d5465da2 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-contexts", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "devDependencies": { "@rocket.chat/core-typings": "workspace:^", diff --git a/packages/ui-video-conf/CHANGELOG.md b/packages/ui-video-conf/CHANGELOG.md index e058c3c4d257..450c89d74361 100644 --- a/packages/ui-video-conf/CHANGELOG.md +++ b/packages/ui-video-conf/CHANGELOG.md @@ -1,5 +1,15 @@ # @rocket.chat/ui-video-conf +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 + - @rocket.chat/ui-avatar@7.0.0-rc.1 +
        + ## 11.0.0-rc.0 ### Minor Changes @@ -13,6 +23,7 @@ - @rocket.chat/ui-avatar@7.0.0-rc.0 - @rocket.chat/ui-contexts@11.0.0-rc.0 + ## 10.0.1 ### Patch Changes diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index 74c3ec43bcbb..fa14113988a2 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/ui-video-conf", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "devDependencies": { "@babel/core": "~7.22.20", @@ -39,8 +39,8 @@ "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", "@rocket.chat/styled": "*", - "@rocket.chat/ui-avatar": "7.0.0-rc.0", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-avatar": "7.0.0-rc.1", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "react": "^17.0.2", "react-dom": "^17.0.2" }, diff --git a/packages/web-ui-registration/CHANGELOG.md b/packages/web-ui-registration/CHANGELOG.md index c53cb5a2623b..01be06de3c00 100644 --- a/packages/web-ui-registration/CHANGELOG.md +++ b/packages/web-ui-registration/CHANGELOG.md @@ -1,5 +1,14 @@ # @rocket.chat/web-ui-registration +## 11.0.0-rc.1 + +### Patch Changes + +-
        Updated dependencies []: + + - @rocket.chat/ui-contexts@11.0.0-rc.1 +
        + ## 11.0.0-rc.0 ### Patch Changes diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 23fd80c89842..c039b3fc6c94 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/web-ui-registration", - "version": "11.0.0-rc.0", + "version": "11.0.0-rc.1", "private": true, "homepage": "https://rocket.chat", "main": "./dist/index.js", @@ -47,7 +47,7 @@ "peerDependencies": { "@rocket.chat/layout": "*", "@rocket.chat/tools": "0.2.2", - "@rocket.chat/ui-contexts": "11.0.0-rc.0", + "@rocket.chat/ui-contexts": "11.0.0-rc.1", "@tanstack/react-query": "*", "react": "*", "react-hook-form": "*", From 7dc9c4154f672c26f046e8936f4cb02e86814355 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 25 Sep 2024 13:14:54 -0300 Subject: [PATCH 45/57] chore: replace Meteor._localStorage -> Accounts.storageLocation (#33356) --- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 27 ++++++++++--------- apps/meteor/app/ui-master/server/scripts.ts | 1 + .../client/messageBox/createComposerAPI.ts | 8 +++--- apps/meteor/app/ui-utils/server/Message.ts | 4 +-- .../app/utils/client/lib/RestApiClient.ts | 6 +++-- .../client/meteorOverrides/login/saml.ts | 2 +- .../providers/UserProvider/UserProvider.tsx | 6 ++--- apps/meteor/client/startup/accounts.ts | 16 +---------- .../components/AuthorizationFormPage.tsx | 3 +-- .../externals/meteor/accounts-base.d.ts | 1 + 10 files changed, 31 insertions(+), 43 deletions(-) diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index bbd6f208f35a..50224cb89dbb 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -6,6 +6,7 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import _ from 'lodash'; +import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; @@ -308,8 +309,8 @@ class E2E extends Emitter { getKeysFromLocalStorage(): KeyPair { return { - public_key: Meteor._localStorage.getItem('public_key'), - private_key: Meteor._localStorage.getItem('private_key'), + public_key: Accounts.storageLocation.getItem('public_key'), + private_key: Accounts.storageLocation.getItem('private_key'), }; } @@ -332,7 +333,7 @@ class E2E extends Emitter { imperativeModal.close(); }, onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); + Accounts.storageLocation.removeItem('e2e.randomPassword'); this.setState(E2EEState.READY); dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); this.closeAlert(); @@ -394,7 +395,7 @@ class E2E extends Emitter { await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } - const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); + const randomPassword = Accounts.storageLocation.getItem('e2e.randomPassword'); if (randomPassword) { this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ @@ -412,8 +413,8 @@ class E2E extends Emitter { this.log('-> Stop Client'); this.closeAlert(); - Meteor._localStorage.removeItem('public_key'); - Meteor._localStorage.removeItem('private_key'); + Accounts.storageLocation.removeItem('public_key'); + Accounts.storageLocation.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; this.started = false; @@ -425,8 +426,8 @@ class E2E extends Emitter { async changePassword(newPassword: string): Promise { await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true }); - if (Meteor._localStorage.getItem('e2e.randomPassword')) { - Meteor._localStorage.setItem('e2e.randomPassword', newPassword); + if (Accounts.storageLocation.getItem('e2e.randomPassword')) { + Accounts.storageLocation.setItem('e2e.randomPassword', newPassword); } } @@ -447,12 +448,12 @@ class E2E extends Emitter { } async loadKeys({ public_key, private_key }: { public_key: string; private_key: string }): Promise { - Meteor._localStorage.setItem('public_key', public_key); + Accounts.storageLocation.setItem('public_key', public_key); try { this.privateKey = await importRSAKey(EJSON.parse(private_key), ['decrypt']); - Meteor._localStorage.setItem('private_key', private_key); + Accounts.storageLocation.setItem('private_key', private_key); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); @@ -474,7 +475,7 @@ class E2E extends Emitter { try { const publicKey = await exportJWKKey(key.publicKey); - Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); + Accounts.storageLocation.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); @@ -483,7 +484,7 @@ class E2E extends Emitter { try { const privateKey = await exportJWKKey(key.privateKey); - Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); + Accounts.storageLocation.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); @@ -498,7 +499,7 @@ class E2E extends Emitter { async createRandomPassword(): Promise { const randomPassword = await generateMnemonicPhrase(5); - Meteor._localStorage.setItem('e2e.randomPassword', randomPassword); + Accounts.storageLocation.setItem('e2e.randomPassword', randomPassword); return randomPassword; } diff --git a/apps/meteor/app/ui-master/server/scripts.ts b/apps/meteor/app/ui-master/server/scripts.ts index 9edadb021d32..3e84a6e39c90 100644 --- a/apps/meteor/app/ui-master/server/scripts.ts +++ b/apps/meteor/app/ui-master/server/scripts.ts @@ -45,6 +45,7 @@ window.addEventListener('load', function() { }); window.localStorage.clear(); Meteor._localStorage = window.sessionStorage; + Accounts.config({ clientStorage: 'session' }); } }); ` diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index a926f8540d27..741f7959fa90 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; -import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI'; import { withDebouncing } from '../../../../lib/utils/highOrderFunctions'; @@ -31,11 +31,11 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const persist = withDebouncing({ wait: 300 })(() => { if (input.value) { - Meteor._localStorage.setItem(storageID, input.value); + Accounts.storageLocation.setItem(storageID, input.value); return; } - Meteor._localStorage.removeItem(storageID); + Accounts.storageLocation.removeItem(storageID); }); const notifyQuotedMessagesUpdate = (): void => { @@ -262,7 +262,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string) const insertNewLine = (): void => insertText('\n'); - setText(Meteor._localStorage.getItem(storageID) ?? '', { + setText(Accounts.storageLocation.getItem(storageID) ?? '', { skipFocus: true, }); diff --git a/apps/meteor/app/ui-utils/server/Message.ts b/apps/meteor/app/ui-utils/server/Message.ts index 06ae59238b42..21d8886c70bc 100644 --- a/apps/meteor/app/ui-utils/server/Message.ts +++ b/apps/meteor/app/ui-utils/server/Message.ts @@ -1,6 +1,6 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Meteor } from 'meteor/meteor'; +import { Accounts } from 'meteor/accounts-base'; import { trim } from '../../../lib/utils/stringUtils'; import { i18n } from '../../../server/lib/i18n'; @@ -17,7 +17,7 @@ export const Message = { } if (messageType.message) { if (!language) { - language = Meteor._localStorage.getItem('userLanguage') || 'en'; + language = Accounts.storageLocation.getItem('userLanguage') || 'en'; } const data = (typeof messageType.data === 'function' && messageType.data(msg)) || {}; return i18n.t(messageType.message, { ...data, lng: language }); diff --git a/apps/meteor/app/utils/client/lib/RestApiClient.ts b/apps/meteor/app/utils/client/lib/RestApiClient.ts index c5e12250b441..53c95ee3e4fa 100644 --- a/apps/meteor/app/utils/client/lib/RestApiClient.ts +++ b/apps/meteor/app/utils/client/lib/RestApiClient.ts @@ -1,6 +1,5 @@ import { RestClient } from '@rocket.chat/api-client'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import { invokeTwoFactorModal } from '../../../../client/lib/2fa/process2faReturn'; import { baseURI } from '../../../../client/lib/baseURI'; @@ -12,7 +11,10 @@ class RestApiClient extends RestClient { 'X-Auth-Token': string; } | undefined { - const [uid, token] = [Meteor._localStorage.getItem(Accounts.USER_ID_KEY), Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY)]; + const [uid, token] = [ + Accounts.storageLocation.getItem(Accounts.USER_ID_KEY), + Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY), + ]; if (!uid || !token) { return; diff --git a/apps/meteor/client/meteorOverrides/login/saml.ts b/apps/meteor/client/meteorOverrides/login/saml.ts index 14dfcc694e5c..f2199af5c0c7 100644 --- a/apps/meteor/client/meteorOverrides/login/saml.ts +++ b/apps/meteor/client/meteorOverrides/login/saml.ts @@ -72,7 +72,7 @@ Meteor.logout = async function (...args) { // Remove the userId from the client to prevent calls to the server while the logout is processed. // If the logout fails, the userId will be reloaded on the resume call - Meteor._localStorage.removeItem(Accounts.USER_ID_KEY); + Accounts.storageLocation.removeItem(Accounts.USER_ID_KEY); // A nasty bounce: 'result' has the SAML LogoutRequest but we need a proper 302 to redirected from the server. window.location.replace(Meteor.absoluteUrl(`_saml/sloRedirect/${provider}/?redirect=${encodeURIComponent(result)}`)); diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 27bba21eae95..53761fbef4e7 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -19,8 +19,6 @@ import { useDeleteUser } from './hooks/useDeleteUser'; import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning'; import { useUpdateAvatar } from './hooks/useUpdateAvatar'; -const getUserId = (): string | null => Meteor.userId(); - const getUser = (): IUser | null => Meteor.user() as IUser | null; const logout = (): Promise => @@ -42,9 +40,9 @@ type UserProviderProps = { }; const UserProvider = ({ children }: UserProviderProps): ReactElement => { - const userId = useReactiveValue(getUserId); - const previousUserId = useRef(userId); const user = useReactiveValue(getUser); + const userId = user?._id ?? null; + const previousUserId = useRef(userId); const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', ''); const [preferedLanguage, setPreferedLanguage] = useLocalStorage('preferedLanguage', ''); diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 88008a606656..50c033dc0596 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -2,7 +2,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import { settings } from '../../app/settings/client'; +// import { settings } from '../../app/settings/client'; import { mainReady } from '../../app/ui-utils/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; @@ -25,17 +25,3 @@ Accounts.onEmailVerificationLink((token: string) => { }); }); }); - -Meteor.startup(() => { - Tracker.autorun((computation) => { - const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); - - if (forgetUserSessionOnWindowClose === undefined) { - return; - } - - computation.stop(); - - Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); - }); -}); diff --git a/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx b/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx index 14f251042abc..623214352372 100644 --- a/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx +++ b/apps/meteor/client/views/oauth/components/AuthorizationFormPage.tsx @@ -4,7 +4,6 @@ import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { Form } from '@rocket.chat/layout'; import { useLogout, useRoute } from '@rocket.chat/ui-contexts'; import { Accounts } from 'meteor/accounts-base'; -import { Meteor } from 'meteor/meteor'; import React, { useEffect, useMemo, useRef } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -19,7 +18,7 @@ type AuthorizationFormPageProps = { }; const AuthorizationFormPage = ({ oauthApp, redirectUri, user }: AuthorizationFormPageProps) => { - const token = useMemo(() => Meteor._localStorage.getItem(Accounts.LOGIN_TOKEN_KEY) ?? undefined, []); + const token = useMemo(() => Accounts.storageLocation.getItem(Accounts.LOGIN_TOKEN_KEY) ?? undefined, []); const formLabelId = useUniqueId(); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 31b70f7b7154..0d30eed0430d 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -1,5 +1,6 @@ declare module 'meteor/accounts-base' { namespace Accounts { + const storageLocation: Window['localStorage']; function createUser( options: { username?: string; From b4c3e5cfee9f5a778b26c374b8b89a5a1ae79a26 Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 25 Sep 2024 19:43:42 -0300 Subject: [PATCH 46/57] Bump rocket.chat to 6.14.0-develop (#33366) --- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- packages/core-typings/package.json | 2 +- packages/rest-typings/package.json | 2 +- yarn.lock | 22 +++++++++++----------- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index cfec7fa33824..de3eb50fa5d1 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "6.13.0-rc.1" + "version": "6.14.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 766770427e0a..3767939a7e3c 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "private": true, "author": { "name": "Rocket.Chat", diff --git a/package.json b/package.json index 1a87fa6034bf..f06deed440ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 6fcd5a43d20d..0db23760066b 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:^", "eslint": "~8.45.0", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 6d435a93ccfc..30a448980c9b 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "6.13.0-rc.1", + "version": "6.14.0-develop", "devDependencies": { "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~29.5.13", diff --git a/yarn.lock b/yarn.lock index 161420f46399..f6c5de3e9a44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8962,10 +8962,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 11.0.0-rc.0 + "@rocket.chat/ui-video-conf": 11.0.0-rc.1 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9052,8 +9052,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.31-rc.0 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 11.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-client": 11.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 katex: "*" react: "*" languageName: unknown @@ -10286,7 +10286,7 @@ __metadata: typescript: ~5.5.4 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10337,8 +10337,8 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: "*" react-i18next: "*" languageName: unknown @@ -10507,8 +10507,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 7.0.0-rc.0 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-avatar": 7.0.0-rc.1 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10596,7 +10596,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 11.0.0-rc.0 + "@rocket.chat/ui-contexts": 11.0.0-rc.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 6bee2a11a5ca3ed282311d0dd7f97d9c0a9392a0 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:54:59 -0700 Subject: [PATCH 47/57] ci: use node20 for release action (#33343) --- packages/release-action/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/release-action/action.yml b/packages/release-action/action.yml index 2fc8fe35d565..23d7382aab6d 100644 --- a/packages/release-action/action.yml +++ b/packages/release-action/action.yml @@ -10,7 +10,7 @@ inputs: required: false runs: - using: "node16" + using: "node20" main: "dist/index.js" branding: From fc26d85b287e9f3c7b8b8f714316cfb72470048e Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 26 Sep 2024 18:47:39 -0300 Subject: [PATCH 48/57] regression: `Sidepanel` sort requires refresh after room update (#33370) --- .../views/room/Sidepanel/hooks/useTeamslistChildren.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts index 5791a6e5d547..772c29509608 100644 --- a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -1,6 +1,7 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import type { Mongo } from 'meteor/mongo'; import { useEffect, useMemo } from 'react'; import { ChatRoom } from '../../../../../app/models/client'; @@ -12,7 +13,7 @@ const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { if (!b.lm) { return -1; } - return new Date(b.lm).toUTCString().localeCompare(new Date(a.lm).toUTCString()); + return b.lm.getTime() - a.lm.getTime(); }; export const useTeamsListChildrenUpdate = ( @@ -23,7 +24,7 @@ export const useTeamsListChildrenUpdate = ( const queryClient = useQueryClient(); const query = useMemo(() => { - const query: Parameters[0] = { + const query: Mongo.Selector = { $or: [ { _id: parentRid, From fa226e4fcc3f1e4f785dd45f0e615e13b7775100 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 27 Sep 2024 01:49:52 -0300 Subject: [PATCH 49/57] chore: add ui-composer to storybook (#33383) --- .gitignore | 1 + packages/ui-composer/package.json | 102 +++++++++++++++--------------- 2 files changed, 53 insertions(+), 50 deletions(-) diff --git a/.gitignore b/.gitignore index dbad2c29a22c..03e74631957b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ yarn-error.log* .env.test.local .env.production.local +storybook-static # turbo .turbo diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index c43f3485880e..562865bfd2d1 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -1,52 +1,54 @@ { - "name": "@rocket.chat/ui-composer", - "version": "0.3.0-rc.0", - "private": true, - "main": "./dist/index.js", - "typings": "./dist/index.d.ts", - "files": [ - "/dist" - ], - "scripts": { - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", - "build": "rm -rf dist && tsc -p tsconfig.build.json", - "typecheck": "tsc --noEmit", - "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "storybook": "start-storybook -p 6006" - }, - "devDependencies": { - "@babel/core": "~7.22.20", - "@react-aria/toolbar": "^3.0.0-beta.1", - "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/fuselage": "^0.59.1", - "@rocket.chat/icons": "~0.38.0", - "@storybook/addon-actions": "~6.5.16", - "@storybook/addon-docs": "~6.5.16", - "@storybook/addon-essentials": "~6.5.16", - "@storybook/builder-webpack4": "~6.5.16", - "@storybook/manager-webpack4": "~6.5.16", - "@storybook/react": "~6.5.16", - "@storybook/testing-library": "~0.0.13", - "@types/react": "~17.0.80", - "@types/react-dom": "~17.0.25", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.32.2", - "eslint-plugin-react-hooks": "~4.6.2", - "eslint-plugin-storybook": "~0.6.15", - "react": "~17.0.2", - "react-docgen-typescript-plugin": "~1.0.8", - "react-dom": "~17.0.2", - "typescript": "~5.5.4" - }, - "peerDependencies": { - "@react-aria/toolbar": "*", - "@rocket.chat/fuselage": "*", - "@rocket.chat/icons": "*", - "react": "^17.0.2", - "react-dom": "^17.0.2" - }, - "volta": { - "extends": "../../package.json" - } + "name": "@rocket.chat/ui-composer", + "version": "0.3.0-rc.0", + "private": true, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "build": "rm -rf dist && tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit", + "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", + "storybook": "start-storybook -p 6006", + "build-preview": "build-storybook", + ".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./storybook-static ../../.preview/ui-composer" + }, + "devDependencies": { + "@babel/core": "~7.22.20", + "@react-aria/toolbar": "^3.0.0-beta.1", + "@rocket.chat/eslint-config": "workspace:^", + "@rocket.chat/fuselage": "^0.59.1", + "@rocket.chat/icons": "~0.38.0", + "@storybook/addon-actions": "~6.5.16", + "@storybook/addon-docs": "~6.5.16", + "@storybook/addon-essentials": "~6.5.16", + "@storybook/builder-webpack4": "~6.5.16", + "@storybook/manager-webpack4": "~6.5.16", + "@storybook/react": "~6.5.16", + "@storybook/testing-library": "~0.0.13", + "@types/react": "~17.0.80", + "@types/react-dom": "~17.0.25", + "eslint": "~8.45.0", + "eslint-plugin-react": "~7.32.2", + "eslint-plugin-react-hooks": "~4.6.2", + "eslint-plugin-storybook": "~0.6.15", + "react": "~17.0.2", + "react-docgen-typescript-plugin": "~1.0.8", + "react-dom": "~17.0.2", + "typescript": "~5.5.4" + }, + "peerDependencies": { + "@react-aria/toolbar": "*", + "@rocket.chat/fuselage": "*", + "@rocket.chat/icons": "*", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "volta": { + "extends": "../../package.json" + } } From 34087b04728c52038cfd3dc7c5412c1fa35d58e3 Mon Sep 17 00:00:00 2001 From: "Julio A." <52619625+julio-cfa@users.noreply.github.com> Date: Fri, 27 Sep 2024 14:46:04 +0200 Subject: [PATCH 50/57] ci: remove Jira-GitHub security integration (#33384) --- .../vulnerabilities-jira-integration.yml | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/vulnerabilities-jira-integration.yml diff --git a/.github/workflows/vulnerabilities-jira-integration.yml b/.github/workflows/vulnerabilities-jira-integration.yml deleted file mode 100644 index 2daeb533937d..000000000000 --- a/.github/workflows/vulnerabilities-jira-integration.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Github vulnerabilities and jira board integration - -on: - schedule: - - cron: '0 1 * * *' - -jobs: - IntegrateSecurityVulnerabilities: - runs-on: ubuntu-latest - steps: - - name: "Github vulnerabilities and jira board integration" - uses: RocketChat/github-vulnerabilities-jira-integration@v0.3 - env: - JIRA_URL: https://rocketchat.atlassian.net/ - JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} - GITHUB_TOKEN: ${{ secrets._GITHUB_TOKEN }} - JIRA_EMAIL: security-team-accounts@rocket.chat - JIRA_PROJECT_ID: GJIT - UID_CUSTOMFIELD_ID: customfield_10059 - JIRA_COMPLETE_PHASE_ID: 31 - JIRA_START_PHASE_ID: 11 - From 5965a1dbfe568f3379f53202a888932e6ffd3e52 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 27 Sep 2024 10:47:09 -0300 Subject: [PATCH 51/57] chore: Single Contact ID: improved typings and removed some duplicated code (#33324) --- .../app/livechat/server/api/v1/contact.ts | 14 +-- .../app/livechat/server/lib/Contacts.ts | 87 ++++++++----------- .../app/livechat/server/lib/LivechatTyped.ts | 4 +- .../server/models/raw/LivechatCustomField.ts | 6 +- 4 files changed, 50 insertions(+), 61 deletions(-) diff --git a/apps/meteor/app/livechat/server/api/v1/contact.ts b/apps/meteor/app/livechat/server/api/v1/contact.ts index f3fec80b23fe..ec0559d5f191 100644 --- a/apps/meteor/app/livechat/server/api/v1/contact.ts +++ b/apps/meteor/app/livechat/server/api/v1/contact.ts @@ -9,7 +9,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { API } from '../../../../api/server'; -import { Contacts, createContact, updateContact } from '../../lib/Contacts'; +import { Contacts, createContact, updateContact, isSingleContactEnabled } from '../../lib/Contacts'; API.v1.addRoute( 'omnichannel/contact', @@ -96,8 +96,8 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps }, { async post() { - if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { - throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); } const contactId = await createContact({ ...this.bodyParams, unknown: false }); @@ -111,8 +111,8 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps }, { async post() { - if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { - throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); } const contact = await updateContact({ ...this.bodyParams }); @@ -127,8 +127,8 @@ API.v1.addRoute( { authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps }, { async get() { - if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') { - throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode'); + if (!isSingleContactEnabled()) { + return API.v1.unauthorized(); } const contact = await LivechatContacts.findOneById(this.queryParams.contactId); diff --git a/apps/meteor/app/livechat/server/lib/Contacts.ts b/apps/meteor/app/livechat/server/lib/Contacts.ts index f6f812ce8af8..e9be40aa942b 100644 --- a/apps/meteor/app/livechat/server/lib/Contacts.ts +++ b/apps/meteor/app/livechat/server/lib/Contacts.ts @@ -1,4 +1,5 @@ import type { + AtLeast, ILivechatContact, ILivechatContactChannel, ILivechatCustomField, @@ -113,41 +114,8 @@ export const Contacts = { } } - const allowedCF = LivechatCustomField.findByScope>( - 'visitor', - { - projection: { _id: 1, label: 1, regexp: 1, required: 1 }, - }, - false, - ); - - const livechatData: Record = {}; - - for await (const cf of allowedCF) { - if (!customFields.hasOwnProperty(cf._id)) { - if (cf.required) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - continue; - } - const cfValue: string = trim(customFields[cf._id]); - - if (!cfValue || typeof cfValue !== 'string') { - if (cf.required) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - continue; - } - - if (cf.regexp) { - const regex = new RegExp(cf.regexp); - if (!regex.test(cfValue)) { - throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); - } - } - - livechatData[cf._id] = cfValue; - } + const allowedCF = await getAllowedCustomFields(); + const livechatData: Record = validateCustomFields(allowedCF, customFields, { ignoreAdditionalFields: true }); const fieldsToRemove = { // if field is explicitely set to empty string, remove @@ -202,15 +170,20 @@ export const Contacts = { }, }; +export function isSingleContactEnabled(): boolean { + // The Single Contact feature is not yet available in production, but can already be partially used in test environments. + return process.env.TEST_MODE?.toUpperCase() === 'TRUE'; +} + export async function createContact(params: CreateContactParams): Promise { - const { name, emails, phones, customFields = {}, contactManager, channels, unknown } = params; + const { name, emails, phones, customFields: receivedCustomFields = {}, contactManager, channels, unknown } = params; if (contactManager) { await validateContactManager(contactManager); } const allowedCustomFields = await getAllowedCustomFields(); - validateCustomFields(allowedCustomFields, customFields); + const customFields = validateCustomFields(allowedCustomFields, receivedCustomFields); const { insertedId } = await LivechatContacts.insertOne({ name, @@ -226,7 +199,7 @@ export async function createContact(params: CreateContactParams): Promise { - const { contactId, name, emails, phones, customFields, contactManager, channels } = params; + const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels } = params; const contact = await LivechatContacts.findOneById>(contactId, { projection: { _id: 1 } }); @@ -238,17 +211,21 @@ export async function updateContact(params: UpdateContactParams): Promise { +async function getAllowedCustomFields(): Promise[]> { return LivechatCustomField.findByScope( 'visitor', { @@ -258,7 +235,13 @@ async function getAllowedCustomFields(): Promise { ).toArray(); } -export function validateCustomFields(allowedCustomFields: ILivechatCustomField[], customFields: Record) { +export function validateCustomFields( + allowedCustomFields: AtLeast[], + customFields: Record, + options?: { ignoreAdditionalFields?: boolean }, +): Record { + const validValues: Record = {}; + for (const cf of allowedCustomFields) { if (!customFields.hasOwnProperty(cf._id)) { if (cf.required) { @@ -281,14 +264,20 @@ export function validateCustomFields(allowedCustomFields: ILivechatCustomField[] throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label })); } } + + validValues[cf._id] = cfValue; } - const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); - for (const key in customFields) { - if (!allowedCustomFieldIds.has(key)) { - throw new Error(i18n.t('error-custom-field-not-allowed', { key })); + if (!options?.ignoreAdditionalFields) { + const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id)); + for (const key in customFields) { + if (!allowedCustomFieldIds.has(key)) { + throw new Error(i18n.t('error-custom-field-not-allowed', { key })); + } } } + + return validValues; } export async function validateContactManager(contactManagerUserId: string) { diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts index 6c2d655f4c95..44ee46f04418 100644 --- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -71,7 +71,7 @@ import * as Mailer from '../../../mailer/server/api'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; import { businessHourManager } from '../business-hour'; -import { createContact } from './Contacts'; +import { createContact, isSingleContactEnabled } from './Contacts'; import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -669,7 +669,7 @@ class LivechatClass { } } - if (process.env.TEST_MODE?.toUpperCase() === 'TRUE') { + if (isSingleContactEnabled()) { const contactId = await createContact({ name: name ?? (visitorDataToUpdate.username as string), emails: email ? [email] : [], diff --git a/apps/meteor/server/models/raw/LivechatCustomField.ts b/apps/meteor/server/models/raw/LivechatCustomField.ts index 71228f55069d..38a93f6439b4 100644 --- a/apps/meteor/server/models/raw/LivechatCustomField.ts +++ b/apps/meteor/server/models/raw/LivechatCustomField.ts @@ -13,12 +13,12 @@ export class LivechatCustomFieldRaw extends BaseRaw implem return [{ key: { scope: 1 } }]; } - findByScope( + findByScope( scope: ILivechatCustomField['scope'], options?: FindOptions, includeHidden = true, - ): FindCursor { - return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options); + ): FindCursor { + return this.find({ scope, ...(includeHidden === true ? {} : { visibility: { $ne: 'hidden' } }) }, options); } findMatchingCustomFields( From 92e366ee285a2f6adec4bafa45c3bc21994cf1e3 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 27 Sep 2024 14:45:31 -0300 Subject: [PATCH 52/57] fix: race condition when forwarding livechat by splitting subscription removal (#33381) --- .changeset/dry-taxis-cry.md | 5 +++++ apps/meteor/server/models/raw/BaseRaw.ts | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .changeset/dry-taxis-cry.md diff --git a/.changeset/dry-taxis-cry.md b/.changeset/dry-taxis-cry.md new file mode 100644 index 000000000000..ae8244087d9e --- /dev/null +++ b/.changeset/dry-taxis-cry.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a race condition that causes livechat conversations to get stuck in the agent's sidebar panel after being forwarded. diff --git a/apps/meteor/server/models/raw/BaseRaw.ts b/apps/meteor/server/models/raw/BaseRaw.ts index d822038b177e..3e763017bbd3 100644 --- a/apps/meteor/server/models/raw/BaseRaw.ts +++ b/apps/meteor/server/models/raw/BaseRaw.ts @@ -318,33 +318,33 @@ export abstract class BaseRaw< async findOneAndDelete(filter: Filter, options?: FindOneAndDeleteOptions): Promise> { if (!this.trash) { - if (options) { - return this.col.findOneAndDelete(filter, options); - } - return this.col.findOneAndDelete(filter); + return this.col.findOneAndDelete(filter, options || {}); } - const result = await this.col.findOneAndDelete(filter); - - const { value: doc } = result; + const doc = await this.col.findOne(filter); if (!doc) { - return result; + return { ok: 1, value: null }; } const { _id, ...record } = doc; - const trash: TDeleted = { ...record, _deletedAt: new Date(), __collection__: this.name, } as unknown as TDeleted; - // since the operation is not atomic, we need to make sure that the record is not already deleted/inserted await this.trash?.updateOne({ _id } as Filter, { $set: trash } as UpdateFilter, { upsert: true, }); - return result; + try { + await this.col.deleteOne({ _id } as Filter); + } catch (e) { + await this.trash?.deleteOne({ _id } as Filter); + throw e; + } + + return { ok: 1, value: doc }; } async deleteMany(filter: Filter, options?: DeleteOptions & { onTrash?: (record: ResultFields) => void }): Promise { From b17b3befdf860eeb532d9dfd78903ab15a709b2f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 27 Sep 2024 14:11:21 -0600 Subject: [PATCH 53/57] fix: Avoid notifying `watch.settings` on uncaught errors (#33376) --- .../server/lib/RocketChat.ErrorHandler.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts index 264443a1378b..984561fe13cd 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.ts @@ -3,14 +3,13 @@ import { Meteor } from 'meteor/meteor'; import { throttledCounter } from '../../../../lib/utils/throttledCounter'; import { sendMessage } from '../../../lib/server/functions/sendMessage'; -import { notifyOnSettingChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; const incException = throttledCounter((counter) => { Settings.incrementValueById('Uncaught_Exceptions_Count', counter, { returnDocument: 'after' }) .then(({ value }) => { if (value) { - void notifyOnSettingChanged(value); + settings.set(value); } }) .catch(console.error); @@ -118,5 +117,12 @@ process.on('unhandledRejection', (error) => { process.on('uncaughtException', async (error) => { incException(); + + console.error('=== UnCaughtException ==='); + console.error(error); + console.error('-------------------------'); + console.error('Errors like this can cause oplog processing errors.'); + console.error('==========================='); + void errorHandler.trackError(error.message, error.stack); }); From a430faf72bb160355bf7cb92549e27d341e35d7c Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 27 Sep 2024 18:52:13 -0300 Subject: [PATCH 54/57] chore: add gazzodown preview (#33279) --- .gitignore | 2 ++ packages/gazzodown/.storybook/main.js | 8 ++++++++ packages/gazzodown/package.json | 2 ++ 3 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index 03e74631957b..8ca2d018f92e 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ storybook-static data/ registration.yaml + +storybook-static diff --git a/packages/gazzodown/.storybook/main.js b/packages/gazzodown/.storybook/main.js index de5a951bbded..f12b58cf856f 100644 --- a/packages/gazzodown/.storybook/main.js +++ b/packages/gazzodown/.storybook/main.js @@ -15,6 +15,14 @@ module.exports = { include: /node_modules/, loader: 'babel-loader', }); + config.module.rules.push({ + test: /\.m?js$/, + include: /node_modules/, + type: 'javascript/auto', + use: { + loader: require.resolve('babel-loader'), + }, + }); return config; }, }; diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index cf4a9209d649..ca64df5ec913 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -10,6 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "build-storybook": "build-storybook", + "build-preview": "build-storybook --quiet", + ".:build-preview-move": "mkdir -p ../../.preview && cp -r ./storybook-static ../../.preview/gazzodown", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint --ext .js,.jsx,.ts,.tsx .", "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", From 214d1b348681bb43f6c1b859b31efe5773bb2cce Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 27 Sep 2024 19:11:27 -0300 Subject: [PATCH 55/57] chore(Sidepanel): uses only local channels and discussions (#33387) --- .../views/room/Sidepanel/RoomSidepanel.tsx | 4 +- .../SidepanelItem/RoomSidepanelItem.tsx | 4 +- .../Sidepanel/hooks/useTeamslistChildren.ts | 79 ++++--------------- 3 files changed, 20 insertions(+), 67 deletions(-) diff --git a/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx index 27c45e2774e8..6ee16a850202 100644 --- a/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx +++ b/apps/meteor/client/views/room/Sidepanel/RoomSidepanel.tsx @@ -51,8 +51,8 @@ const RoomSidepanelWithData = ({ parentRid, openedRoom }: { parentRid: string; o ( diff --git a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx index dceb69e1aba3..8bb4d84eaebe 100644 --- a/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx +++ b/apps/meteor/client/views/room/Sidepanel/SidepanelItem/RoomSidepanelItem.tsx @@ -1,4 +1,4 @@ -import type { IRoom, ISubscription, Serialized } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; import { useUserSubscription } from '@rocket.chat/ui-contexts'; import React, { memo } from 'react'; @@ -8,7 +8,7 @@ import { useItemData } from '../hooks/useItemData'; export type RoomSidepanelItemProps = { openedRoom?: string; - room: Serialized; + room: IRoom; parentRid: string; viewMode?: 'extended' | 'medium' | 'condensed'; }; diff --git a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts index 772c29509608..de7645ae2a30 100644 --- a/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts +++ b/apps/meteor/client/views/room/Sidepanel/hooks/useTeamslistChildren.ts @@ -1,107 +1,60 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { useEndpoint } from '@rocket.chat/ui-contexts'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import type { Mongo } from 'meteor/mongo'; import { useEffect, useMemo } from 'react'; import { ChatRoom } from '../../../../../app/models/client'; -const sortRoomByLastMessage = (a: IRoom, b: IRoom) => { - if (!a.lm) { - return 1; - } - if (!b.lm) { - return -1; - } - return b.lm.getTime() - a.lm.getTime(); -}; - export const useTeamsListChildrenUpdate = ( parentRid: string, teamId?: string | null, sidepanelItems?: 'channels' | 'discussions' | null, ) => { - const queryClient = useQueryClient(); - const query = useMemo(() => { const query: Mongo.Selector = { $or: [ { _id: parentRid, }, - { - prid: parentRid, - }, ], }; - if (teamId && query.$or) { + if ((!sidepanelItems || sidepanelItems === 'discussions') && query.$or) { + query.$or.push({ + prid: parentRid, + }); + } + + if ((!sidepanelItems || sidepanelItems === 'channels') && teamId && query.$or) { query.$or.push({ teamId, }); } return query; - }, [parentRid, teamId]); + }, [parentRid, teamId, sidepanelItems]); - const teamList = useEndpoint('GET', '/v1/teams.listChildren'); - - const listRoomsAndDiscussions = useEndpoint('GET', '/v1/teams.listChildren'); const result = useQuery({ queryKey: ['sidepanel', 'list', parentRid, sidepanelItems], queryFn: () => - listRoomsAndDiscussions({ - roomId: parentRid, - sort: JSON.stringify({ lm: -1 }), - type: sidepanelItems || undefined, - }), + ChatRoom.find(query, { + sort: { lm: -1 }, + }).fetch(), enabled: sidepanelItems !== null && teamId !== null, refetchInterval: 5 * 60 * 1000, keepPreviousData: true, }); - const { mutate: update } = useMutation({ - mutationFn: async (params?: { action: 'add' | 'remove' | 'update'; data: IRoom }) => { - queryClient.setQueryData(['sidepanel', 'list', parentRid, sidepanelItems], (data: Awaited> | void) => { - if (!data) { - return; - } - - if (params?.action === 'add') { - data.data = [JSON.parse(JSON.stringify(params.data)), ...data.data].sort(sortRoomByLastMessage); - } - - if (params?.action === 'remove') { - data.data = data.data.filter((item) => item._id !== params.data?._id); - } - - if (params?.action === 'update') { - data.data = data.data - .map((item) => (item._id === params.data?._id ? JSON.parse(JSON.stringify(params.data)) : item)) - .sort(sortRoomByLastMessage); - } - - return { ...data }; - }); - }, - }); - useEffect(() => { const liveQueryHandle = ChatRoom.find(query).observe({ - added: (item) => { - queueMicrotask(() => update({ action: 'add', data: item })); - }, - changed: (item) => { - queueMicrotask(() => update({ action: 'update', data: item })); - }, - removed: (item) => { - queueMicrotask(() => update({ action: 'remove', data: item })); - }, + added: () => queueMicrotask(() => result.refetch({ exact: false })), + changed: () => queueMicrotask(() => result.refetch({ exact: false })), + removed: () => queueMicrotask(() => result.refetch({ exact: false })), }); return () => { liveQueryHandle.stop(); }; - }, [update, query]); + }, [query, result]); return result; }; From bc1e6eec3be194a017624f73449c5974d0e296a1 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Fri, 27 Sep 2024 19:36:57 -0300 Subject: [PATCH 56/57] chore: Move Apps-Engine to monorepo (#32951) --- .github/CODEOWNERS | 1 + .github/actions/build-docker-image/action.yml | 5 +- .github/actions/build-docker/action.yml | 7 + .github/actions/meteor-build/action.yml | 5 + .github/actions/setup-node/action.yml | 25 +- .github/workflows/ci-code-check.yml | 4 + .../workflows/ci-deploy-gh-pages-preview.yml | 1 + .github/workflows/ci-deploy-gh-pages.yml | 1 + .github/workflows/ci-test-e2e.yml | 4 + .github/workflows/ci-test-unit.yml | 4 + .github/workflows/ci.yml | 19 + .github/workflows/new-release.yml | 1 + .github/workflows/pr-update-description.yml | 1 + .github/workflows/publish-release.yml | 1 + .github/workflows/release-candidate.yml | 1 + .tool-versions | 1 + README.md | 1 + apps/meteor/.docker/Dockerfile | 11 +- apps/meteor/.docker/Dockerfile.alpine | 27 +- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- docker-compose-ci.yml | 4 + ee/apps/account-service/Dockerfile | 4 + ee/apps/authorization-service/Dockerfile | 4 + ee/apps/ddp-streamer/Dockerfile | 4 + ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/Dockerfile | 4 + ee/apps/presence-service/Dockerfile | 4 + ee/apps/queue-worker/Dockerfile | 4 + ee/apps/stream-hub-service/Dockerfile | 4 + ee/packages/presence/package.json | 2 +- packages/apps-engine/.eslintignore | 8 + packages/apps-engine/.eslintrc.json | 58 + packages/apps-engine/.gitignore | 61 + packages/apps-engine/.prettierrc | 7 + packages/apps-engine/README.md | 125 ++ packages/apps-engine/deno-runtime/.gitignore | 1 + .../deno-runtime/AppObjectRegistry.ts | 26 + .../apps-engine/deno-runtime/acorn-walk.d.ts | 170 ++ packages/apps-engine/deno-runtime/acorn.d.ts | 857 ++++++++++ packages/apps-engine/deno-runtime/deno.jsonc | 16 + packages/apps-engine/deno-runtime/deno.lock | 107 ++ .../deno-runtime/handlers/api-handler.ts | 46 + .../deno-runtime/handlers/app/construct.ts | 126 ++ .../handlers/app/handleGetStatus.ts | 15 + .../handlers/app/handleInitialize.ts | 19 + .../handlers/app/handleOnDisable.ts | 19 + .../handlers/app/handleOnEnable.ts | 16 + .../handlers/app/handleOnInstall.ts | 30 + .../handlers/app/handleOnPreSettingUpdate.ts | 22 + .../handlers/app/handleOnSettingUpdated.ts | 24 + .../handlers/app/handleOnUninstall.ts | 30 + .../handlers/app/handleOnUpdate.ts | 30 + .../handlers/app/handleSetStatus.ts | 29 + .../deno-runtime/handlers/app/handler.ts | 112 ++ .../deno-runtime/handlers/listener/handler.ts | 150 ++ .../handlers/scheduler-handler.ts | 51 + .../handlers/slashcommand-handler.ts | 122 ++ .../handlers/tests/api-handler.test.ts | 79 + .../handlers/tests/listener-handler.test.ts | 234 +++ .../handlers/tests/scheduler-handler.test.ts | 46 + .../tests/slashcommand-handler.test.ts | 152 ++ .../handlers/tests/uikit-handler.test.ts | 99 ++ .../tests/videoconference-handler.test.ts | 122 ++ .../deno-runtime/handlers/uikit/handler.ts | 82 + .../handlers/videoconference-handler.ts | 49 + .../lib/accessors/builders/BlockBuilder.ts | 216 +++ .../accessors/builders/DiscussionBuilder.ts | 59 + .../builders/LivechatMessageBuilder.ts | 204 +++ .../lib/accessors/builders/MessageBuilder.ts | 232 +++ .../lib/accessors/builders/RoomBuilder.ts | 163 ++ .../lib/accessors/builders/UserBuilder.ts | 81 + .../builders/VideoConferenceBuilder.ts | 94 ++ .../lib/accessors/extenders/HttpExtender.ts | 62 + .../accessors/extenders/MessageExtender.ts | 66 + .../lib/accessors/extenders/RoomExtender.ts | 61 + .../extenders/VideoConferenceExtend.ts | 69 + .../deno-runtime/lib/accessors/http.ts | 92 ++ .../deno-runtime/lib/accessors/mod.ts | 302 ++++ .../lib/accessors/modify/ModifyCreator.ts | 344 ++++ .../lib/accessors/modify/ModifyExtender.ts | 93 ++ .../lib/accessors/modify/ModifyUpdater.ts | 153 ++ .../deno-runtime/lib/accessors/notifier.ts | 75 + .../lib/accessors/tests/AppAccessors.test.ts | 122 ++ .../lib/accessors/tests/ModifyCreator.test.ts | 106 ++ .../accessors/tests/ModifyExtender.test.ts | 120 ++ .../lib/accessors/tests/ModifyUpdater.test.ts | 128 ++ .../apps-engine/deno-runtime/lib/ast/mod.ts | 64 + .../deno-runtime/lib/ast/operations.ts | 239 +++ .../lib/ast/tests/data/ast_blocks.ts | 436 +++++ .../lib/ast/tests/operations.test.ts | 245 +++ .../apps-engine/deno-runtime/lib/codec.ts | 43 + .../apps-engine/deno-runtime/lib/logger.ts | 142 ++ .../apps-engine/deno-runtime/lib/messenger.ts | 199 +++ .../apps-engine/deno-runtime/lib/require.ts | 14 + packages/apps-engine/deno-runtime/lib/room.ts | 104 ++ .../deno-runtime/lib/roomFactory.ts | 27 + .../lib/sanitizeDeprecatedUsage.ts | 20 + .../deno-runtime/lib/tests/logger.test.ts | 111 ++ .../deno-runtime/lib/tests/messenger.test.ts | 96 ++ packages/apps-engine/deno-runtime/main.ts | 129 ++ packages/apps-engine/package.json | 132 ++ packages/apps-engine/scripts/bundle.js | 35 + packages/apps-engine/scripts/deno-cache.js | 25 + .../src/client/AppClientManager.ts | 28 + .../src/client/AppServerCommunicator.ts | 16 + .../src/client/AppsEngineUIClient.ts | 70 + .../src/client/AppsEngineUIHost.ts | 78 + .../apps-engine/src/client/constants/index.ts | 6 + .../client/definition/AppsEngineUIMethods.ts | 7 + .../definition/IAppsEngineUIResponse.ts | 19 + .../definition/IExternalComponentRoomInfo.ts | 16 + .../definition/IExternalComponentUserInfo.ts | 14 + .../src/client/definition/index.ts | 4 + packages/apps-engine/src/client/index.ts | 4 + .../apps-engine/src/client/utils/index.ts | 18 + packages/apps-engine/src/definition/App.ts | 236 +++ .../apps-engine/src/definition/AppStatus.ts | 65 + packages/apps-engine/src/definition/IApp.ts | 90 + packages/apps-engine/src/definition/LICENSE | 21 + .../src/definition/accessors/IApiExtend.ts | 16 + .../src/definition/accessors/IAppAccessors.ts | 11 + .../accessors/IAppInstallationContext.ts | 5 + .../accessors/IAppUninstallationContext.ts | 5 + .../definition/accessors/IAppUpdateContext.ts | 6 + .../accessors/ICloudWorkspaceRead.ts | 24 + .../accessors/IConfigurationExtend.ts | 36 + .../accessors/IConfigurationModify.ts | 18 + .../accessors/IDiscussionBuilder.ts | 25 + .../src/definition/accessors/IEmailCreator.ts | 10 + .../definition/accessors/IEnvironmentRead.ts | 27 + .../definition/accessors/IEnvironmentWrite.ts | 10 + .../accessors/IEnvironmentalVariableRead.ts | 11 + .../accessors/IExternalComponentsExtend.ts | 17 + .../src/definition/accessors/IHttp.ts | 202 +++ .../definition/accessors/ILivechatCreator.ts | 43 + .../accessors/ILivechatMessageBuilder.ts | 219 +++ .../src/definition/accessors/ILivechatRead.ts | 38 + .../definition/accessors/ILivechatUpdater.ts | 33 + .../src/definition/accessors/ILogEntry.ts | 22 + .../src/definition/accessors/ILogger.ts | 29 + .../definition/accessors/IMessageBuilder.ts | 236 +++ .../definition/accessors/IMessageExtender.ts | 36 + .../src/definition/accessors/IMessageRead.ts | 15 + .../definition/accessors/IMessageUpdater.ts | 21 + .../definition/accessors/IModerationModify.ts | 27 + .../src/definition/accessors/IModify.ts | 45 + .../definition/accessors/IModifyCreator.ts | 100 ++ .../definition/accessors/IModifyDeleter.ts | 12 + .../definition/accessors/IModifyExtender.ts | 40 + .../definition/accessors/IModifyUpdater.ts | 52 + .../src/definition/accessors/INotifier.ts | 63 + .../src/definition/accessors/IOAuthApp.ts | 13 + .../definition/accessors/IOAuthAppsModify.ts | 23 + .../definition/accessors/IOAuthAppsReader.ts | 16 + .../src/definition/accessors/IPersistence.ts | 97 ++ .../definition/accessors/IPersistenceRead.ts | 40 + .../src/definition/accessors/IRead.ts | 52 + .../src/definition/accessors/IRoleRead.ts | 27 + .../src/definition/accessors/IRoomBuilder.ts | 186 +++ .../src/definition/accessors/IRoomExtender.ts | 40 + .../src/definition/accessors/IRoomRead.ts | 93 ++ .../definition/accessors/ISchedulerExtend.ts | 11 + .../definition/accessors/ISchedulerModify.ts | 31 + .../accessors/IServerSettingRead.ts | 43 + .../accessors/IServerSettingUpdater.ts | 6 + .../accessors/IServerSettingsModify.ts | 40 + .../src/definition/accessors/ISettingRead.ts | 23 + .../definition/accessors/ISettingUpdater.ts | 5 + .../definition/accessors/ISettingsExtend.ts | 17 + .../accessors/ISlashCommandsExtend.ts | 16 + .../accessors/ISlashCommandsModify.ts | 30 + .../src/definition/accessors/IThreadRead.ts | 9 + .../src/definition/accessors/IUIController.ts | 31 + .../src/definition/accessors/IUIExtend.ts | 5 + .../definition/accessors/IUploadCreator.ts | 12 + .../src/definition/accessors/IUploadRead.ts | 7 + .../src/definition/accessors/IUserBuilder.ts | 60 + .../src/definition/accessors/IUserRead.ts | 22 + .../src/definition/accessors/IUserUpdater.ts | 18 + .../accessors/IVideoConfProvidersExtend.ts | 15 + .../accessors/IVideoConferenceBuilder.ts | 34 + .../accessors/IVideoConferenceExtend.ts | 21 + .../accessors/IVideoConferenceRead.ts | 15 + .../src/definition/accessors/index.ts | 58 + .../src/definition/api/ApiEndpoint.ts | 40 + .../apps-engine/src/definition/api/IApi.ts | 58 + .../src/definition/api/IApiEndpoint.ts | 47 + .../src/definition/api/IApiEndpointInfo.ts | 6 + .../definition/api/IApiEndpointMetadata.ts | 10 + .../src/definition/api/IApiExample.ts | 19 + .../src/definition/api/IRequest.ts | 16 + .../src/definition/api/IResponse.ts | 13 + .../apps-engine/src/definition/api/index.ts | 8 + .../src/definition/app-schema.json | 75 + .../src/definition/assets/IAsset.ts | 6 + .../src/definition/assets/IAssetProvider.ts | 5 + .../src/definition/assets/index.ts | 4 + .../src/definition/cloud/IWorkspaceToken.ts | 4 + .../src/definition/email/IEmail.ts | 9 + .../src/definition/email/IEmailDescriptor.ts | 11 + .../src/definition/email/IPreEmailSent.ts | 25 + .../definition/email/IPreEmailSentContext.ts | 6 + .../apps-engine/src/definition/email/index.ts | 4 + .../src/definition/example-app.json | 13 + .../exceptions/AppsEngineException.ts | 32 + .../EssentialAppDisabledException.ts | 16 + .../FileUploadNotAllowedException.ts | 12 + .../InvalidSettingValueException.ts | 8 + .../exceptions/UserNotAllowedException.ts | 14 + .../src/definition/exceptions/index.ts | 5 + .../externalComponent/IExternalComponent.ts | 51 + .../IExternalComponentOptions.ts | 10 + .../IExternalComponentState.ts | 16 + .../IPostExternalComponentClosed.ts | 16 + .../IPostExternalComponentOpened.ts | 16 + .../src/definition/externalComponent/index.ts | 5 + .../src/definition/livechat/IDepartment.ts | 17 + .../livechat/ILivechatEventContext.ts | 7 + .../definition/livechat/ILivechatMessage.ts | 7 + .../src/definition/livechat/ILivechatRoom.ts | 55 + .../livechat/ILivechatRoomClosedHandler.ts | 19 + .../livechat/ILivechatTransferData.ts | 8 + .../livechat/ILivechatTransferEventContext.ts | 15 + .../livechat/IPostLivechatAgentAssigned.ts | 25 + .../livechat/IPostLivechatAgentUnassigned.ts | 25 + .../livechat/IPostLivechatGuestSaved.ts | 19 + .../livechat/IPostLivechatRoomClosed.ts | 19 + .../livechat/IPostLivechatRoomSaved.ts | 19 + .../livechat/IPostLivechatRoomStarted.ts | 19 + .../livechat/IPostLivechatRoomTransferred.ts | 13 + .../src/definition/livechat/IVisitor.ts | 16 + .../src/definition/livechat/IVisitorEmail.ts | 3 + .../src/definition/livechat/IVisitorPhone.ts | 3 + .../src/definition/livechat/index.ts | 38 + .../src/definition/messages/IMessage.ts | 34 + .../src/definition/messages/IMessageAction.ts | 17 + .../definition/messages/IMessageAttachment.ts | 43 + .../messages/IMessageAttachmentAuthor.ts | 11 + .../messages/IMessageAttachmentField.ts | 11 + .../messages/IMessageAttachmentTitle.ts | 8 + .../messages/IMessageDeleteContext.ts | 17 + .../src/definition/messages/IMessageFile.ts | 5 + .../messages/IMessageFollowContext.ts | 21 + .../definition/messages/IMessagePinContext.ts | 21 + .../src/definition/messages/IMessageRaw.ts | 40 + .../definition/messages/IMessageReaction.ts | 13 + .../messages/IMessageReactionContext.ts | 25 + .../messages/IMessageReportContext.ts | 21 + .../messages/IMessageStarContext.ts | 21 + .../messages/IPostMessageDeleted.ts | 37 + .../messages/IPostMessageFollowed.ts | 18 + .../definition/messages/IPostMessagePinned.ts | 18 + .../messages/IPostMessageReacted.ts | 19 + .../messages/IPostMessageReported.ts | 18 + .../definition/messages/IPostMessageSent.ts | 27 + .../messages/IPostMessageSentToBot.ts | 15 + .../messages/IPostMessageStarred.ts | 18 + .../messages/IPostMessageUpdated.ts | 27 + .../messages/IPreMessageDeletePrevent.ts | 28 + .../messages/IPreMessageSentExtend.ts | 29 + .../messages/IPreMessageSentModify.ts | 29 + .../messages/IPreMessageSentPrevent.ts | 28 + .../messages/IPreMessageUpdatedExtend.ts | 29 + .../messages/IPreMessageUpdatedModify.ts | 29 + .../messages/IPreMessageUpdatedPrevent.ts | 28 + .../messages/MessageActionButtonsAlignment.ts | 4 + .../definition/messages/MessageActionType.ts | 3 + .../messages/MessageProcessingType.ts | 4 + .../src/definition/messages/index.ts | 71 + .../src/definition/metadata/AppInterface.ts | 60 + .../src/definition/metadata/AppMethod.ts | 102 ++ .../src/definition/metadata/IAppAuthorInfo.ts | 5 + .../src/definition/metadata/IAppInfo.ts | 20 + .../metadata/RocketChatAssociations.ts | 22 + .../src/definition/metadata/index.ts | 8 + .../src/definition/oauth2/IOAuth2.ts | 136 ++ .../src/definition/oauth2/OAuth2.ts | 15 + .../src/definition/permissions/IPermission.ts | 12 + .../persistence/IPersistenceItem.ts | 7 + .../src/definition/persistence/index.ts | 1 + .../apps-engine/src/definition/roles/IRole.ts | 8 + .../apps-engine/src/definition/roles/index.ts | 5 + .../src/definition/rooms/IPostRoomCreate.ts | 27 + .../src/definition/rooms/IPostRoomDeleted.ts | 27 + .../definition/rooms/IPostRoomUserJoined.ts | 19 + .../definition/rooms/IPostRoomUserLeave.ts | 19 + .../definition/rooms/IPreRoomCreateExtend.ts | 28 + .../definition/rooms/IPreRoomCreateModify.ts | 28 + .../definition/rooms/IPreRoomCreatePrevent.ts | 26 + .../definition/rooms/IPreRoomDeletePrevent.ts | 26 + .../definition/rooms/IPreRoomUserJoined.ts | 18 + .../src/definition/rooms/IPreRoomUserLeave.ts | 18 + .../apps-engine/src/definition/rooms/IRoom.ts | 26 + .../rooms/IRoomUserJoinedContext.ts | 23 + .../definition/rooms/IRoomUserLeaveContext.ts | 23 + .../src/definition/rooms/RoomType.ts | 6 + .../apps-engine/src/definition/rooms/index.ts | 17 + .../definition/scheduler/IOnetimeSchedule.ts | 13 + .../src/definition/scheduler/IProcessor.ts | 45 + .../scheduler/IRecurringSchedule.ts | 17 + .../src/definition/scheduler/index.ts | 3 + .../src/definition/settings/ISetting.ts | 47 + .../settings/ISettingUpdateContext.ts | 6 + .../src/definition/settings/SettingType.ts | 14 + .../src/definition/settings/index.ts | 4 + .../definition/slashcommands/ISlashCommand.ts | 41 + .../slashcommands/ISlashCommandPreview.ts | 28 + .../slashcommands/SlashCommandContext.ts | 33 + .../src/definition/slashcommands/index.ts | 5 + .../ui/IUIActionButtonDescriptor.ts | 41 + .../definition/ui/UIActionButtonContext.ts | 7 + .../apps-engine/src/definition/ui/index.ts | 2 + .../definition/uikit/IUIKitActionHandler.ts | 77 + .../uikit/IUIKitIncomingInteraction.ts | 28 + .../IUIKitIncomingInteractionActionButton.ts | 78 + .../definition/uikit/IUIKitInteractionType.ts | 41 + .../src/definition/uikit/IUIKitSurface.ts | 22 + .../src/definition/uikit/IUIKitView.ts | 7 + .../UIKitIncomingInteractionContainer.ts | 18 + .../uikit/UIKitIncomingInteractionTypes.ts | 60 + .../uikit/UIKitInteractionContext.ts | 69 + .../uikit/UIKitInteractionPayloadFormatter.ts | 68 + .../uikit/UIKitInteractionResponder.ts | 75 + .../definition/uikit/blocks/BlockBuilder.ts | 196 +++ .../src/definition/uikit/blocks/Blocks.ts | 108 ++ .../src/definition/uikit/blocks/Elements.ts | 115 ++ .../src/definition/uikit/blocks/Objects.ts | 33 + .../src/definition/uikit/blocks/index.ts | 4 + .../apps-engine/src/definition/uikit/index.ts | 7 + .../livechat/IUIKitLivechatActionHandler.ts | 23 + .../IUIKitLivechatIncomingInteraction.ts | 17 + .../UIKitLivechatIncomingInteractionType.ts | 22 + .../UIKitLivechatInteractionContext.ts | 33 + .../src/definition/uikit/livechat/index.ts | 4 + .../definition/uploads/IFileUploadContext.ts | 6 + .../src/definition/uploads/IPreFileUpload.ts | 20 + .../src/definition/uploads/IUpload.ts | 25 + .../definition/uploads/IUploadDescriptor.ts | 24 + .../src/definition/uploads/IUploadDetails.ts | 26 + .../src/definition/uploads/StoreType.ts | 7 + .../src/definition/uploads/index.ts | 6 + .../src/definition/users/IBotUser.ts | 6 + .../src/definition/users/IPostUserCreated.ts | 15 + .../src/definition/users/IPostUserDeleted.ts | 15 + .../src/definition/users/IPostUserLoggedIn.ts | 15 + .../definition/users/IPostUserLoggedOut.ts | 15 + .../users/IPostUserStatusChanged.ts | 17 + .../src/definition/users/IPostUserUpdated.ts | 15 + .../apps-engine/src/definition/users/IUser.ts | 25 + .../src/definition/users/IUserContext.ts | 19 + .../definition/users/IUserCreationOptions.ts | 7 + .../src/definition/users/IUserEmail.ts | 4 + .../src/definition/users/IUserLookup.ts | 5 + .../src/definition/users/IUserSettings.ts | 5 + .../definition/users/IUserStatusContext.ts | 23 + .../src/definition/users/IUserUpdateContex.ts | 24 + .../definition/users/IUserUpdateContext.ts | 24 + .../definition/users/UserStatusConnection.ts | 9 + .../src/definition/users/UserType.ts | 10 + .../apps-engine/src/definition/users/index.ts | 33 + .../videoConfProviders/IVideoConfProvider.ts | 63 + .../IVideoConferenceOptions.ts | 4 + .../videoConfProviders/VideoConfData.ts | 7 + .../definition/videoConfProviders/index.ts | 5 + .../videoConferences/AppVideoConference.ts | 6 + .../videoConferences/IVideoConference.ts | 56 + .../videoConferences/IVideoConferenceUser.ts | 5 + .../src/definition/videoConferences/index.ts | 5 + packages/apps-engine/src/lib/utils.ts | 1 + packages/apps-engine/src/server/AppManager.ts | 1150 +++++++++++++ .../apps-engine/src/server/IGetAppsFilter.ts | 6 + packages/apps-engine/src/server/ProxiedApp.ts | 154 ++ .../src/server/accessors/ApiExtend.ts | 11 + .../src/server/accessors/AppAccessors.ts | 36 + .../server/accessors/CloudWorkspaceRead.ts | 11 + .../server/accessors/ConfigurationExtend.ts | 24 + .../server/accessors/ConfigurationModify.ts | 9 + .../src/server/accessors/DiscussionBuilder.ts | 47 + .../src/server/accessors/EmailCreator.ts | 11 + .../src/server/accessors/EnvironmentRead.ts | 21 + .../src/server/accessors/EnvironmentWrite.ts | 13 + .../accessors/EnvironmentalVariableRead.ts | 18 + .../accessors/ExternalComponentsExtend.ts | 11 + .../apps-engine/src/server/accessors/Http.ts | 77 + .../src/server/accessors/HttpExtend.ts | 58 + .../src/server/accessors/LivechatCreator.ts | 29 + .../accessors/LivechatMessageBuilder.ts | 191 +++ .../src/server/accessors/LivechatRead.ts | 73 + .../src/server/accessors/LivechatUpdater.ts | 23 + .../src/server/accessors/MessageBuilder.ts | 224 +++ .../src/server/accessors/MessageExtender.ts | 50 + .../src/server/accessors/MessageRead.ts | 33 + .../src/server/accessors/ModerationModify.ts | 20 + .../src/server/accessors/Modify.ts | 89 + .../src/server/accessors/ModifyCreator.ts | 260 +++ .../src/server/accessors/ModifyDeleter.ts | 35 + .../src/server/accessors/ModifyExtender.ts | 46 + .../src/server/accessors/ModifyUpdater.ts | 106 ++ .../src/server/accessors/Notifier.ts | 49 + .../src/server/accessors/OAuthAppsModify.ts | 19 + .../src/server/accessors/OAuthAppsReader.ts | 15 + .../src/server/accessors/Persistence.ts | 43 + .../src/server/accessors/PersistenceRead.ts | 19 + .../src/server/accessors/Reader.ts | 87 + .../src/server/accessors/RoleRead.ts | 15 + .../src/server/accessors/RoomBuilder.ts | 155 ++ .../src/server/accessors/RoomExtender.ts | 56 + .../src/server/accessors/RoomRead.ts | 73 + .../src/server/accessors/SchedulerExtend.ts | 11 + .../src/server/accessors/SchedulerModify.ts | 27 + .../src/server/accessors/ServerSettingRead.ts | 34 + .../server/accessors/ServerSettingUpdater.ts | 15 + .../server/accessors/ServerSettingsModify.ts | 23 + .../src/server/accessors/SettingRead.ts | 25 + .../src/server/accessors/SettingUpdater.ts | 22 + .../src/server/accessors/SettingsExtend.ts | 26 + .../server/accessors/SlashCommandsExtend.ts | 11 + .../server/accessors/SlashCommandsModify.ts | 19 + .../src/server/accessors/ThreadRead.ts | 11 + .../src/server/accessors/UIController.ts | 106 ++ .../src/server/accessors/UIExtend.ts | 11 + .../src/server/accessors/UploadCreator.ts | 25 + .../src/server/accessors/UploadRead.ts | 21 + .../src/server/accessors/UserBuilder.ts | 74 + .../src/server/accessors/UserRead.ts | 23 + .../src/server/accessors/UserUpdater.ts | 28 + .../accessors/VideoConfProviderExtend.ts | 11 + .../accessors/VideoConferenceBuilder.ts | 83 + .../server/accessors/VideoConferenceExtend.ts | 64 + .../server/accessors/VideoConferenceRead.ts | 11 + .../apps-engine/src/server/accessors/index.ts | 95 ++ .../src/server/bridges/ApiBridge.ts | 49 + .../src/server/bridges/AppActivationBridge.ts | 35 + .../src/server/bridges/AppBridges.ts | 101 ++ .../server/bridges/AppDetailChangesBridge.ts | 16 + .../src/server/bridges/BaseBridge.ts | 6 + .../server/bridges/CloudWorkspaceBridge.ts | 30 + .../src/server/bridges/CommandBridge.ts | 117 ++ .../src/server/bridges/EmailBridge.ts | 30 + .../bridges/EnvironmentalVariableBridge.ts | 45 + .../src/server/bridges/HttpBridge.ts | 37 + .../src/server/bridges/IInternalBridge.ts | 7 + .../bridges/IInternalFederationBridge.ts | 15 + .../bridges/IInternalPersistenceBridge.ts | 9 + .../bridges/IInternalSchedulerBridge.ts | 8 + .../src/server/bridges/IInternalUserBridge.ts | 8 + .../src/server/bridges/IListenerBridge.ts | 10 + .../src/server/bridges/InternalBridge.ts | 22 + .../src/server/bridges/ListenerBridge.ts | 18 + .../src/server/bridges/LivechatBridge.ts | 259 +++ .../src/server/bridges/MessageBridge.ts | 116 ++ .../src/server/bridges/ModerationBridge.ts | 47 + .../src/server/bridges/OAuthAppsBridge.ts | 85 + .../src/server/bridges/PersistenceBridge.ts | 161 ++ .../src/server/bridges/RoleBridge.ts | 38 + .../src/server/bridges/RoomBridge.ts | 179 ++ .../src/server/bridges/SchedulerBridge.ts | 62 + .../src/server/bridges/ServerSettingBridge.ts | 93 ++ .../src/server/bridges/ThreadBridge.ts | 35 + .../src/server/bridges/UiInteractionBridge.ts | 31 + .../src/server/bridges/UploadBridge.ts | 62 + .../src/server/bridges/UserBridge.ts | 149 ++ .../server/bridges/VideoConferenceBridge.ts | 94 ++ .../apps-engine/src/server/bridges/index.ts | 52 + .../src/server/compiler/AppCompiler.ts | 30 + .../compiler/AppFabricationFulfillment.ts | 74 + .../src/server/compiler/AppImplements.ts | 27 + .../src/server/compiler/AppPackageParser.ts | 161 ++ .../server/compiler/IParseAppPackageResult.ts | 9 + .../apps-engine/src/server/compiler/index.ts | 7 + .../src/server/compiler/modules/index.ts | 55 + .../src/server/compiler/modules/networking.ts | 36 + .../errors/CommandAlreadyExistsError.ts | 9 + .../CommandHasAlreadyBeenTouchedError.ts | 9 + .../src/server/errors/CompilerError.ts | 9 + .../server/errors/InvalidInstallationError.ts | 5 + .../src/server/errors/InvalidLicenseError.ts | 7 + .../server/errors/MustContainFunctionError.ts | 9 + .../src/server/errors/MustExtendAppError.ts | 5 + .../errors/NotEnoughMethodArgumentsError.ts | 9 + .../server/errors/PathAlreadyExistsError.ts | 9 + .../server/errors/PermissionDeniedError.ts | 25 + .../server/errors/RequiredApiVersionError.ts | 21 + .../VideoConfProviderAlreadyExistsError.ts | 9 + .../VideoConfProviderNotRegisteredError.ts | 9 + .../apps-engine/src/server/errors/index.ts | 25 + .../src/server/logging/AppConsole.ts | 121 ++ .../src/server/logging/ILoggerStorageEntry.ts | 14 + .../apps-engine/src/server/logging/index.ts | 4 + .../src/server/managers/AppAccessorManager.ts | 228 +++ .../apps-engine/src/server/managers/AppApi.ts | 95 ++ .../src/server/managers/AppApiManager.ts | 165 ++ .../managers/AppExternalComponentManager.ts | 142 ++ .../src/server/managers/AppLicenseManager.ts | 91 + .../src/server/managers/AppListenerManager.ts | 1220 ++++++++++++++ .../server/managers/AppPermissionManager.ts | 40 + .../src/server/managers/AppRuntimeManager.ts | 57 + .../server/managers/AppSchedulerManager.ts | 98 ++ .../src/server/managers/AppSettingsManager.ts | 55 + .../server/managers/AppSignatureManager.ts | 85 + .../src/server/managers/AppSlashCommand.ts | 77 + .../server/managers/AppSlashCommandManager.ts | 470 ++++++ .../server/managers/AppVideoConfProvider.ts | 105 ++ .../managers/AppVideoConfProviderManager.ts | 207 +++ .../server/managers/UIActionButtonManager.ts | 72 + .../apps-engine/src/server/managers/index.ts | 21 + .../server/marketplace/IAppLicenseMetadata.ts | 5 + .../server/marketplace/IMarketplaceInfo.ts | 24 + .../marketplace/IMarketplacePricingPlan.ts | 11 + .../marketplace/IMarketplacePricingTier.ts | 6 + .../IMarketplaceSimpleBundleInfo.ts | 4 + .../IMarketplaceSubscriptionInfo.ts | 15 + .../marketplace/MarketplacePricingStrategy.ts | 5 + .../marketplace/MarketplacePurchaseType.ts | 4 + .../MarketplaceSubscriptionStatus.ts | 10 + .../MarketplaceSubscriptionType.ts | 4 + .../src/server/marketplace/index.ts | 8 + .../license/AppLicenseValidationResult.ts | 56 + .../src/server/marketplace/license/Crypto.ts | 26 + .../src/server/marketplace/license/index.ts | 4 + .../src/server/messages/Message.ts | 106 ++ .../apps-engine/src/server/misc/UIHelper.ts | 32 + .../apps-engine/src/server/misc/Utilities.ts | 36 + .../src/server/oauth2/OAuth2Client.ts | 322 ++++ .../src/server/permissions/AppPermissions.ts | 152 ++ packages/apps-engine/src/server/rooms/Room.ts | 104 ++ .../server/runtime/AppsEngineEmptyRuntime.ts | 21 + .../server/runtime/AppsEngineNodeRuntime.ts | 71 + .../src/server/runtime/AppsEngineRuntime.ts | 29 + .../runtime/deno/AppsEngineDenoRuntime.ts | 568 +++++++ .../server/runtime/deno/LivenessManager.ts | 184 +++ .../server/runtime/deno/ProcessMessenger.ts | 48 + .../src/server/runtime/deno/bundler.ts | 90 + .../src/server/runtime/deno/codec.ts | 29 + .../src/server/storage/AppLogStorage.ts | 24 + .../src/server/storage/AppMetadataStorage.ts | 19 + .../src/server/storage/AppSourceStorage.ts | 40 + .../src/server/storage/IAppStorageItem.ts | 31 + .../apps-engine/src/server/storage/index.ts | 4 + packages/apps-engine/tests/runner.ts | 20 + .../tests/server/AppManager.spec.ts | 122 ++ .../server/accessors/AppAccessors.spec.ts | 134 ++ .../accessors/ConfigurationExtend.spec.ts | 76 + .../accessors/ConfigurationModify.spec.ts | 28 + .../server/accessors/EnvironmentRead.spec.ts | 29 + .../server/accessors/EnvironmentWrite.spec.ts | 25 + .../EnvironmentalVariableRead.spec.ts | 33 + .../tests/server/accessors/Http.spec.ts | 128 ++ .../tests/server/accessors/HttpExtend.spec.ts | 83 + .../server/accessors/MessageBuilder.spec.ts | 125 ++ .../server/accessors/MessageExtender.spec.ts | 35 + .../server/accessors/MessageRead.spec.ts | 58 + .../tests/server/accessors/Modify.spec.ts | 44 + .../server/accessors/ModifyCreator.spec.ts | 129 ++ .../server/accessors/ModifyExtender.spec.ts | 74 + .../server/accessors/ModifyUpdater.spec.ts | 139 ++ .../tests/server/accessors/Notifier.spec.ts | 37 + .../server/accessors/Persistence.spec.ts | 82 + .../server/accessors/PersistenceRead.spec.ts | 31 + .../tests/server/accessors/Reader.spec.ts | 112 ++ .../server/accessors/RoomBuilder.spec.ts | 90 + .../server/accessors/RoomExtender.spec.ts | 38 + .../tests/server/accessors/RoomRead.spec.ts | 83 + .../accessors/ServerSettingRead.spec.ts | 43 + .../accessors/ServerSettingsModify.spec.ts | 60 + .../server/accessors/SettingRead.spec.ts | 40 + .../server/accessors/SettingsExtend.spec.ts | 54 + .../accessors/SlashCommandsExtend.spec.ts | 47 + .../accessors/SlashCommandsModify.spec.ts | 47 + .../server/accessors/UserBuilder.spec.ts | 58 + .../tests/server/accessors/UserRead.spec.ts | 49 + .../accessors/VideoConfProviderExtend.spec.ts | 38 + .../accessors/VideoConferenceBuilder.spec.ts | 108 ++ .../accessors/VideoConferenceExtend.spec.ts | 81 + .../accessors/VideoConferenceRead.spec.ts | 34 + .../tests/server/compiler/AppCompiler.spec.ts | 24 + .../AppFabricationFulfillment.spec.ts | 48 + .../server/compiler/AppImplements.spec.ts | 19 + .../errors/CommandAlreadyExistsError.spec.ts | 13 + .../CommandHasAlreadyBeenTouchedError.spec.ts | 13 + .../tests/server/errors/CompilerError.spec.ts | 13 + .../errors/MustContainFunctionError.spec.ts | 13 + .../server/errors/MustExtendAppError.spec.ts | 13 + .../NotEnoughMethodArgumentsError.spec.ts | 13 + .../errors/RequiredApiVersionError.spec.ts | 26 + .../tests/server/logging/AppConsole.spec.ts | 79 + .../managers/AppAccessorManager.spec.ts | 166 ++ .../tests/server/managers/AppApi.spec.ts | 28 + .../server/managers/AppApiManager.spec.ts | 233 +++ .../AppExternalComponentManager.spec.ts | 154 ++ .../managers/AppListenerManager.spec.ts | 44 + .../managers/AppSettingsManager.spec.ts | 148 ++ .../server/managers/AppSlashCommand.spec.ts | 29 + .../managers/AppSlashCommandManager.spec.ts | 469 ++++++ .../managers/AppVideoConfProvider.spec.ts | 25 + .../AppVideoConfProviderManager.spec.ts | 435 +++++ .../tests/server/misc/Utilities.spec.ts | 69 + .../DenoRuntimeSubprocessController.spec.ts | 222 +++ .../apps-engine/tests/test-data/README.md | 2 + .../test-data/apps/hello-world-test_0.0.1.zip | Bin 0 -> 10309 bytes .../test-data/apps/testing-app_0.0.8.zip | Bin 0 -> 37318 bytes .../test-data/bridges/OAuthAppsBridge.ts | 28 + .../test-data/bridges/activationBridge.ts | 25 + .../tests/test-data/bridges/apiBridge.ts | 37 + .../tests/test-data/bridges/appBridges.ts | 228 +++ .../test-data/bridges/appDetailChanges.ts | 6 + .../tests/test-data/bridges/cloudBridge.ts | 15 + .../tests/test-data/bridges/commandBridge.ts | 38 + .../tests/test-data/bridges/emailBridge.ts | 8 + .../bridges/environmentalVariableBridge.ts | 15 + .../tests/test-data/bridges/httpBridge.ts | 15 + .../tests/test-data/bridges/internalBridge.ts | 16 + .../bridges/internalFederationBridge.ts | 11 + .../tests/test-data/bridges/livechatBridge.ts | 96 ++ .../tests/test-data/bridges/messageBridge.ts | 43 + .../test-data/bridges/moderationBridge.ts | 17 + .../tests/test-data/bridges/persisBridge.ts | 40 + .../tests/test-data/bridges/roleBridge.ts | 12 + .../tests/test-data/bridges/roomBridge.ts | 67 + .../test-data/bridges/schedulerBridge.ts | 24 + .../test-data/bridges/serverSettingBridge.ts | 41 + .../tests/test-data/bridges/threadBridge.ts | 8 + .../test-data/bridges/uiIntegrationBridge.ts | 9 + .../tests/test-data/bridges/uploadBridge.ts | 17 + .../tests/test-data/bridges/userBridge.ts | 44 + .../bridges/videoConferenceBridge.ts | 25 + .../test-data/misc/fake-library-file.d.ts | 5 + .../test-data/storage/TestSourceStorage.ts | 20 + .../tests/test-data/storage/logStorage.ts | 25 + .../tests/test-data/storage/storage.ts | 93 ++ .../apps-engine/tests/test-data/utilities.ts | 492 ++++++ packages/apps-engine/tsconfig-lint.json | 7 + packages/apps-engine/tsconfig.json | 21 + packages/apps-engine/turbo.json | 9 + packages/apps-engine/typedoc.json | 9 + packages/apps/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/fuselage-ui-kit/package.json | 2 +- packages/jwt/package.json | 3 + packages/rest-typings/package.json | 2 +- yarn.lock | 1461 +++++++++++++++-- 643 files changed, 35661 insertions(+), 132 deletions(-) create mode 100644 .tool-versions create mode 100644 packages/apps-engine/.eslintignore create mode 100644 packages/apps-engine/.eslintrc.json create mode 100644 packages/apps-engine/.gitignore create mode 100644 packages/apps-engine/.prettierrc create mode 100644 packages/apps-engine/README.md create mode 100644 packages/apps-engine/deno-runtime/.gitignore create mode 100644 packages/apps-engine/deno-runtime/AppObjectRegistry.ts create mode 100644 packages/apps-engine/deno-runtime/acorn-walk.d.ts create mode 100644 packages/apps-engine/deno-runtime/acorn.d.ts create mode 100644 packages/apps-engine/deno-runtime/deno.jsonc create mode 100644 packages/apps-engine/deno-runtime/deno.lock create mode 100644 packages/apps-engine/deno-runtime/handlers/api-handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/construct.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/app/handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/listener/handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/uikit/handler.ts create mode 100644 packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/http.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/mod.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/notifier.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/mod.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/operations.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts create mode 100644 packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/codec.ts create mode 100644 packages/apps-engine/deno-runtime/lib/logger.ts create mode 100644 packages/apps-engine/deno-runtime/lib/messenger.ts create mode 100644 packages/apps-engine/deno-runtime/lib/require.ts create mode 100644 packages/apps-engine/deno-runtime/lib/room.ts create mode 100644 packages/apps-engine/deno-runtime/lib/roomFactory.ts create mode 100644 packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts create mode 100644 packages/apps-engine/deno-runtime/lib/tests/logger.test.ts create mode 100644 packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts create mode 100644 packages/apps-engine/deno-runtime/main.ts create mode 100644 packages/apps-engine/package.json create mode 100644 packages/apps-engine/scripts/bundle.js create mode 100644 packages/apps-engine/scripts/deno-cache.js create mode 100644 packages/apps-engine/src/client/AppClientManager.ts create mode 100644 packages/apps-engine/src/client/AppServerCommunicator.ts create mode 100644 packages/apps-engine/src/client/AppsEngineUIClient.ts create mode 100644 packages/apps-engine/src/client/AppsEngineUIHost.ts create mode 100644 packages/apps-engine/src/client/constants/index.ts create mode 100644 packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts create mode 100644 packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts create mode 100644 packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts create mode 100644 packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts create mode 100644 packages/apps-engine/src/client/definition/index.ts create mode 100644 packages/apps-engine/src/client/index.ts create mode 100644 packages/apps-engine/src/client/utils/index.ts create mode 100644 packages/apps-engine/src/definition/App.ts create mode 100644 packages/apps-engine/src/definition/AppStatus.ts create mode 100644 packages/apps-engine/src/definition/IApp.ts create mode 100644 packages/apps-engine/src/definition/LICENSE create mode 100644 packages/apps-engine/src/definition/accessors/IApiExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppAccessors.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts create mode 100644 packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts create mode 100644 packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IConfigurationModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEmailCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts create mode 100644 packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IHttp.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILogEntry.ts create mode 100644 packages/apps-engine/src/definition/accessors/ILogger.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageExtender.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IMessageUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModerationModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyDeleter.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyExtender.ts create mode 100644 packages/apps-engine/src/definition/accessors/IModifyUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/INotifier.ts create mode 100644 packages/apps-engine/src/definition/accessors/IOAuthApp.ts create mode 100644 packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts create mode 100644 packages/apps-engine/src/definition/accessors/IPersistence.ts create mode 100644 packages/apps-engine/src/definition/accessors/IPersistenceRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoleRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoomBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoomExtender.ts create mode 100644 packages/apps-engine/src/definition/accessors/IRoomRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISchedulerModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IServerSettingRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISettingRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISettingUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISettingsExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts create mode 100644 packages/apps-engine/src/definition/accessors/IThreadRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUIController.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUIExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUploadCreator.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUploadRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUserBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUserRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/IUserUpdater.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts create mode 100644 packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts create mode 100644 packages/apps-engine/src/definition/accessors/index.ts create mode 100644 packages/apps-engine/src/definition/api/ApiEndpoint.ts create mode 100644 packages/apps-engine/src/definition/api/IApi.ts create mode 100644 packages/apps-engine/src/definition/api/IApiEndpoint.ts create mode 100644 packages/apps-engine/src/definition/api/IApiEndpointInfo.ts create mode 100644 packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts create mode 100644 packages/apps-engine/src/definition/api/IApiExample.ts create mode 100644 packages/apps-engine/src/definition/api/IRequest.ts create mode 100644 packages/apps-engine/src/definition/api/IResponse.ts create mode 100644 packages/apps-engine/src/definition/api/index.ts create mode 100644 packages/apps-engine/src/definition/app-schema.json create mode 100644 packages/apps-engine/src/definition/assets/IAsset.ts create mode 100644 packages/apps-engine/src/definition/assets/IAssetProvider.ts create mode 100644 packages/apps-engine/src/definition/assets/index.ts create mode 100644 packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts create mode 100644 packages/apps-engine/src/definition/email/IEmail.ts create mode 100644 packages/apps-engine/src/definition/email/IEmailDescriptor.ts create mode 100644 packages/apps-engine/src/definition/email/IPreEmailSent.ts create mode 100644 packages/apps-engine/src/definition/email/IPreEmailSentContext.ts create mode 100644 packages/apps-engine/src/definition/email/index.ts create mode 100644 packages/apps-engine/src/definition/example-app.json create mode 100644 packages/apps-engine/src/definition/exceptions/AppsEngineException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts create mode 100644 packages/apps-engine/src/definition/exceptions/index.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts create mode 100644 packages/apps-engine/src/definition/externalComponent/index.ts create mode 100644 packages/apps-engine/src/definition/livechat/IDepartment.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatMessage.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatRoom.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts create mode 100644 packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts create mode 100644 packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts create mode 100644 packages/apps-engine/src/definition/livechat/IVisitor.ts create mode 100644 packages/apps-engine/src/definition/livechat/IVisitorEmail.ts create mode 100644 packages/apps-engine/src/definition/livechat/IVisitorPhone.ts create mode 100644 packages/apps-engine/src/definition/livechat/index.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessage.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAction.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachment.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachmentAuthor.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachmentField.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageAttachmentTitle.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageDeleteContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageFile.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageFollowContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessagePinContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageRaw.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageReaction.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageReactionContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageReportContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IMessageStarContext.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageDeleted.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageFollowed.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessagePinned.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageReacted.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageReported.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageSent.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageSentToBot.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageStarred.ts create mode 100644 packages/apps-engine/src/definition/messages/IPostMessageUpdated.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageDeletePrevent.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageSentExtend.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageSentModify.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageSentPrevent.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageUpdatedExtend.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageUpdatedModify.ts create mode 100644 packages/apps-engine/src/definition/messages/IPreMessageUpdatedPrevent.ts create mode 100644 packages/apps-engine/src/definition/messages/MessageActionButtonsAlignment.ts create mode 100644 packages/apps-engine/src/definition/messages/MessageActionType.ts create mode 100644 packages/apps-engine/src/definition/messages/MessageProcessingType.ts create mode 100644 packages/apps-engine/src/definition/messages/index.ts create mode 100644 packages/apps-engine/src/definition/metadata/AppInterface.ts create mode 100644 packages/apps-engine/src/definition/metadata/AppMethod.ts create mode 100644 packages/apps-engine/src/definition/metadata/IAppAuthorInfo.ts create mode 100644 packages/apps-engine/src/definition/metadata/IAppInfo.ts create mode 100644 packages/apps-engine/src/definition/metadata/RocketChatAssociations.ts create mode 100644 packages/apps-engine/src/definition/metadata/index.ts create mode 100644 packages/apps-engine/src/definition/oauth2/IOAuth2.ts create mode 100644 packages/apps-engine/src/definition/oauth2/OAuth2.ts create mode 100644 packages/apps-engine/src/definition/permissions/IPermission.ts create mode 100644 packages/apps-engine/src/definition/persistence/IPersistenceItem.ts create mode 100644 packages/apps-engine/src/definition/persistence/index.ts create mode 100644 packages/apps-engine/src/definition/roles/IRole.ts create mode 100644 packages/apps-engine/src/definition/roles/index.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomCreate.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomDeleted.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomUserJoined.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPostRoomUserLeave.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomCreateExtend.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomCreateModify.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomCreatePrevent.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomDeletePrevent.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomUserJoined.ts create mode 100644 packages/apps-engine/src/definition/rooms/IPreRoomUserLeave.ts create mode 100644 packages/apps-engine/src/definition/rooms/IRoom.ts create mode 100644 packages/apps-engine/src/definition/rooms/IRoomUserJoinedContext.ts create mode 100644 packages/apps-engine/src/definition/rooms/IRoomUserLeaveContext.ts create mode 100644 packages/apps-engine/src/definition/rooms/RoomType.ts create mode 100644 packages/apps-engine/src/definition/rooms/index.ts create mode 100644 packages/apps-engine/src/definition/scheduler/IOnetimeSchedule.ts create mode 100644 packages/apps-engine/src/definition/scheduler/IProcessor.ts create mode 100644 packages/apps-engine/src/definition/scheduler/IRecurringSchedule.ts create mode 100644 packages/apps-engine/src/definition/scheduler/index.ts create mode 100644 packages/apps-engine/src/definition/settings/ISetting.ts create mode 100644 packages/apps-engine/src/definition/settings/ISettingUpdateContext.ts create mode 100644 packages/apps-engine/src/definition/settings/SettingType.ts create mode 100644 packages/apps-engine/src/definition/settings/index.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/ISlashCommand.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/ISlashCommandPreview.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/SlashCommandContext.ts create mode 100644 packages/apps-engine/src/definition/slashcommands/index.ts create mode 100644 packages/apps-engine/src/definition/ui/IUIActionButtonDescriptor.ts create mode 100644 packages/apps-engine/src/definition/ui/UIActionButtonContext.ts create mode 100644 packages/apps-engine/src/definition/ui/index.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitActionHandler.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitIncomingInteraction.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitIncomingInteractionActionButton.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitInteractionType.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitSurface.ts create mode 100644 packages/apps-engine/src/definition/uikit/IUIKitView.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitIncomingInteractionContainer.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitIncomingInteractionTypes.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitInteractionContext.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitInteractionPayloadFormatter.ts create mode 100644 packages/apps-engine/src/definition/uikit/UIKitInteractionResponder.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/BlockBuilder.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/Blocks.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/Elements.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/Objects.ts create mode 100644 packages/apps-engine/src/definition/uikit/blocks/index.ts create mode 100644 packages/apps-engine/src/definition/uikit/index.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/IUIKitLivechatActionHandler.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/IUIKitLivechatIncomingInteraction.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/UIKitLivechatIncomingInteractionType.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/UIKitLivechatInteractionContext.ts create mode 100644 packages/apps-engine/src/definition/uikit/livechat/index.ts create mode 100644 packages/apps-engine/src/definition/uploads/IFileUploadContext.ts create mode 100644 packages/apps-engine/src/definition/uploads/IPreFileUpload.ts create mode 100644 packages/apps-engine/src/definition/uploads/IUpload.ts create mode 100644 packages/apps-engine/src/definition/uploads/IUploadDescriptor.ts create mode 100644 packages/apps-engine/src/definition/uploads/IUploadDetails.ts create mode 100644 packages/apps-engine/src/definition/uploads/StoreType.ts create mode 100644 packages/apps-engine/src/definition/uploads/index.ts create mode 100644 packages/apps-engine/src/definition/users/IBotUser.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserCreated.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserDeleted.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserLoggedIn.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserLoggedOut.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserStatusChanged.ts create mode 100644 packages/apps-engine/src/definition/users/IPostUserUpdated.ts create mode 100644 packages/apps-engine/src/definition/users/IUser.ts create mode 100644 packages/apps-engine/src/definition/users/IUserContext.ts create mode 100644 packages/apps-engine/src/definition/users/IUserCreationOptions.ts create mode 100644 packages/apps-engine/src/definition/users/IUserEmail.ts create mode 100644 packages/apps-engine/src/definition/users/IUserLookup.ts create mode 100644 packages/apps-engine/src/definition/users/IUserSettings.ts create mode 100644 packages/apps-engine/src/definition/users/IUserStatusContext.ts create mode 100644 packages/apps-engine/src/definition/users/IUserUpdateContex.ts create mode 100644 packages/apps-engine/src/definition/users/IUserUpdateContext.ts create mode 100644 packages/apps-engine/src/definition/users/UserStatusConnection.ts create mode 100644 packages/apps-engine/src/definition/users/UserType.ts create mode 100644 packages/apps-engine/src/definition/users/index.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/IVideoConfProvider.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/IVideoConferenceOptions.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/VideoConfData.ts create mode 100644 packages/apps-engine/src/definition/videoConfProviders/index.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/AppVideoConference.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/IVideoConference.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/IVideoConferenceUser.ts create mode 100644 packages/apps-engine/src/definition/videoConferences/index.ts create mode 100644 packages/apps-engine/src/lib/utils.ts create mode 100644 packages/apps-engine/src/server/AppManager.ts create mode 100644 packages/apps-engine/src/server/IGetAppsFilter.ts create mode 100644 packages/apps-engine/src/server/ProxiedApp.ts create mode 100644 packages/apps-engine/src/server/accessors/ApiExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/AppAccessors.ts create mode 100644 packages/apps-engine/src/server/accessors/CloudWorkspaceRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ConfigurationExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/ConfigurationModify.ts create mode 100644 packages/apps-engine/src/server/accessors/DiscussionBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/EmailCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/EnvironmentRead.ts create mode 100644 packages/apps-engine/src/server/accessors/EnvironmentWrite.ts create mode 100644 packages/apps-engine/src/server/accessors/EnvironmentalVariableRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ExternalComponentsExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/Http.ts create mode 100644 packages/apps-engine/src/server/accessors/HttpExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatMessageBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatRead.ts create mode 100644 packages/apps-engine/src/server/accessors/LivechatUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/MessageBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/MessageExtender.ts create mode 100644 packages/apps-engine/src/server/accessors/MessageRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ModerationModify.ts create mode 100644 packages/apps-engine/src/server/accessors/Modify.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyDeleter.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyExtender.ts create mode 100644 packages/apps-engine/src/server/accessors/ModifyUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/Notifier.ts create mode 100644 packages/apps-engine/src/server/accessors/OAuthAppsModify.ts create mode 100644 packages/apps-engine/src/server/accessors/OAuthAppsReader.ts create mode 100644 packages/apps-engine/src/server/accessors/Persistence.ts create mode 100644 packages/apps-engine/src/server/accessors/PersistenceRead.ts create mode 100644 packages/apps-engine/src/server/accessors/Reader.ts create mode 100644 packages/apps-engine/src/server/accessors/RoleRead.ts create mode 100644 packages/apps-engine/src/server/accessors/RoomBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/RoomExtender.ts create mode 100644 packages/apps-engine/src/server/accessors/RoomRead.ts create mode 100644 packages/apps-engine/src/server/accessors/SchedulerExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/SchedulerModify.ts create mode 100644 packages/apps-engine/src/server/accessors/ServerSettingRead.ts create mode 100644 packages/apps-engine/src/server/accessors/ServerSettingUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/ServerSettingsModify.ts create mode 100644 packages/apps-engine/src/server/accessors/SettingRead.ts create mode 100644 packages/apps-engine/src/server/accessors/SettingUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/SettingsExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/SlashCommandsExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/SlashCommandsModify.ts create mode 100644 packages/apps-engine/src/server/accessors/ThreadRead.ts create mode 100644 packages/apps-engine/src/server/accessors/UIController.ts create mode 100644 packages/apps-engine/src/server/accessors/UIExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/UploadCreator.ts create mode 100644 packages/apps-engine/src/server/accessors/UploadRead.ts create mode 100644 packages/apps-engine/src/server/accessors/UserBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/UserRead.ts create mode 100644 packages/apps-engine/src/server/accessors/UserUpdater.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConfProviderExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceBuilder.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceExtend.ts create mode 100644 packages/apps-engine/src/server/accessors/VideoConferenceRead.ts create mode 100644 packages/apps-engine/src/server/accessors/index.ts create mode 100644 packages/apps-engine/src/server/bridges/ApiBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/AppActivationBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/AppBridges.ts create mode 100644 packages/apps-engine/src/server/bridges/AppDetailChangesBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/BaseBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/CloudWorkspaceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/CommandBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/EmailBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/EnvironmentalVariableBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/HttpBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalFederationBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalPersistenceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalSchedulerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IInternalUserBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/IListenerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/InternalBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ListenerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/LivechatBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/MessageBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ModerationBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/OAuthAppsBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/PersistenceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/RoleBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/RoomBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/SchedulerBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ServerSettingBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/ThreadBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/UiInteractionBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/UploadBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/UserBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/VideoConferenceBridge.ts create mode 100644 packages/apps-engine/src/server/bridges/index.ts create mode 100644 packages/apps-engine/src/server/compiler/AppCompiler.ts create mode 100644 packages/apps-engine/src/server/compiler/AppFabricationFulfillment.ts create mode 100644 packages/apps-engine/src/server/compiler/AppImplements.ts create mode 100644 packages/apps-engine/src/server/compiler/AppPackageParser.ts create mode 100644 packages/apps-engine/src/server/compiler/IParseAppPackageResult.ts create mode 100644 packages/apps-engine/src/server/compiler/index.ts create mode 100644 packages/apps-engine/src/server/compiler/modules/index.ts create mode 100644 packages/apps-engine/src/server/compiler/modules/networking.ts create mode 100644 packages/apps-engine/src/server/errors/CommandAlreadyExistsError.ts create mode 100644 packages/apps-engine/src/server/errors/CommandHasAlreadyBeenTouchedError.ts create mode 100644 packages/apps-engine/src/server/errors/CompilerError.ts create mode 100644 packages/apps-engine/src/server/errors/InvalidInstallationError.ts create mode 100644 packages/apps-engine/src/server/errors/InvalidLicenseError.ts create mode 100644 packages/apps-engine/src/server/errors/MustContainFunctionError.ts create mode 100644 packages/apps-engine/src/server/errors/MustExtendAppError.ts create mode 100644 packages/apps-engine/src/server/errors/NotEnoughMethodArgumentsError.ts create mode 100644 packages/apps-engine/src/server/errors/PathAlreadyExistsError.ts create mode 100644 packages/apps-engine/src/server/errors/PermissionDeniedError.ts create mode 100644 packages/apps-engine/src/server/errors/RequiredApiVersionError.ts create mode 100644 packages/apps-engine/src/server/errors/VideoConfProviderAlreadyExistsError.ts create mode 100644 packages/apps-engine/src/server/errors/VideoConfProviderNotRegisteredError.ts create mode 100644 packages/apps-engine/src/server/errors/index.ts create mode 100644 packages/apps-engine/src/server/logging/AppConsole.ts create mode 100644 packages/apps-engine/src/server/logging/ILoggerStorageEntry.ts create mode 100644 packages/apps-engine/src/server/logging/index.ts create mode 100644 packages/apps-engine/src/server/managers/AppAccessorManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppApi.ts create mode 100644 packages/apps-engine/src/server/managers/AppApiManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppExternalComponentManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppLicenseManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppListenerManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppPermissionManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppRuntimeManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSchedulerManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSettingsManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSignatureManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppSlashCommand.ts create mode 100644 packages/apps-engine/src/server/managers/AppSlashCommandManager.ts create mode 100644 packages/apps-engine/src/server/managers/AppVideoConfProvider.ts create mode 100644 packages/apps-engine/src/server/managers/AppVideoConfProviderManager.ts create mode 100644 packages/apps-engine/src/server/managers/UIActionButtonManager.ts create mode 100644 packages/apps-engine/src/server/managers/index.ts create mode 100644 packages/apps-engine/src/server/marketplace/IAppLicenseMetadata.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceInfo.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplacePricingPlan.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplacePricingTier.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceSimpleBundleInfo.ts create mode 100644 packages/apps-engine/src/server/marketplace/IMarketplaceSubscriptionInfo.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplacePricingStrategy.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplacePurchaseType.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionStatus.ts create mode 100644 packages/apps-engine/src/server/marketplace/MarketplaceSubscriptionType.ts create mode 100644 packages/apps-engine/src/server/marketplace/index.ts create mode 100644 packages/apps-engine/src/server/marketplace/license/AppLicenseValidationResult.ts create mode 100644 packages/apps-engine/src/server/marketplace/license/Crypto.ts create mode 100644 packages/apps-engine/src/server/marketplace/license/index.ts create mode 100644 packages/apps-engine/src/server/messages/Message.ts create mode 100644 packages/apps-engine/src/server/misc/UIHelper.ts create mode 100644 packages/apps-engine/src/server/misc/Utilities.ts create mode 100644 packages/apps-engine/src/server/oauth2/OAuth2Client.ts create mode 100644 packages/apps-engine/src/server/permissions/AppPermissions.ts create mode 100644 packages/apps-engine/src/server/rooms/Room.ts create mode 100644 packages/apps-engine/src/server/runtime/AppsEngineEmptyRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/AppsEngineNodeRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/AppsEngineRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/LivenessManager.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/ProcessMessenger.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/bundler.ts create mode 100644 packages/apps-engine/src/server/runtime/deno/codec.ts create mode 100644 packages/apps-engine/src/server/storage/AppLogStorage.ts create mode 100644 packages/apps-engine/src/server/storage/AppMetadataStorage.ts create mode 100644 packages/apps-engine/src/server/storage/AppSourceStorage.ts create mode 100644 packages/apps-engine/src/server/storage/IAppStorageItem.ts create mode 100644 packages/apps-engine/src/server/storage/index.ts create mode 100644 packages/apps-engine/tests/runner.ts create mode 100644 packages/apps-engine/tests/server/AppManager.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/AppAccessors.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ConfigurationExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ConfigurationModify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/EnvironmentRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/EnvironmentWrite.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/EnvironmentalVariableRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Http.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/HttpExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/MessageBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/MessageExtender.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/MessageRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Modify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ModifyCreator.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ModifyExtender.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ModifyUpdater.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Notifier.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Persistence.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/PersistenceRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/Reader.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/RoomBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/RoomExtender.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/RoomRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ServerSettingRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/ServerSettingsModify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SettingRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SettingsExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SlashCommandsExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/SlashCommandsModify.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/UserBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/UserRead.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConfProviderExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConferenceBuilder.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConferenceExtend.spec.ts create mode 100644 packages/apps-engine/tests/server/accessors/VideoConferenceRead.spec.ts create mode 100644 packages/apps-engine/tests/server/compiler/AppCompiler.spec.ts create mode 100644 packages/apps-engine/tests/server/compiler/AppFabricationFulfillment.spec.ts create mode 100644 packages/apps-engine/tests/server/compiler/AppImplements.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/CommandAlreadyExistsError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/CommandHasAlreadyBeenTouchedError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/CompilerError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/MustContainFunctionError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/MustExtendAppError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/NotEnoughMethodArgumentsError.spec.ts create mode 100644 packages/apps-engine/tests/server/errors/RequiredApiVersionError.spec.ts create mode 100644 packages/apps-engine/tests/server/logging/AppConsole.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppAccessorManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppApi.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppApiManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppExternalComponentManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppListenerManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppSettingsManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppVideoConfProvider.spec.ts create mode 100644 packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts create mode 100644 packages/apps-engine/tests/server/misc/Utilities.spec.ts create mode 100644 packages/apps-engine/tests/server/runtime/DenoRuntimeSubprocessController.spec.ts create mode 100644 packages/apps-engine/tests/test-data/README.md create mode 100644 packages/apps-engine/tests/test-data/apps/hello-world-test_0.0.1.zip create mode 100644 packages/apps-engine/tests/test-data/apps/testing-app_0.0.8.zip create mode 100644 packages/apps-engine/tests/test-data/bridges/OAuthAppsBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/activationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/apiBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/appBridges.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/appDetailChanges.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/cloudBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/commandBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/emailBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/environmentalVariableBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/httpBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/internalBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/internalFederationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/livechatBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/messageBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/moderationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/persisBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/roleBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/roomBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/schedulerBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/serverSettingBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/threadBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/uiIntegrationBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/uploadBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/userBridge.ts create mode 100644 packages/apps-engine/tests/test-data/bridges/videoConferenceBridge.ts create mode 100644 packages/apps-engine/tests/test-data/misc/fake-library-file.d.ts create mode 100644 packages/apps-engine/tests/test-data/storage/TestSourceStorage.ts create mode 100644 packages/apps-engine/tests/test-data/storage/logStorage.ts create mode 100644 packages/apps-engine/tests/test-data/storage/storage.ts create mode 100644 packages/apps-engine/tests/test-data/utilities.ts create mode 100644 packages/apps-engine/tsconfig-lint.json create mode 100644 packages/apps-engine/tsconfig.json create mode 100644 packages/apps-engine/turbo.json create mode 100644 packages/apps-engine/typedoc.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a834776aeff5..f66c5d29de5b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1,5 @@ /packages/* @RocketChat/Architecture +/packages/apps-engine/ @RocketChat/apps /packages/core-typings/ @RocketChat/Architecture /packages/rest-typings/ @RocketChat/Architecture @RocketChat/backend /packages/ui-contexts/ @RocketChat/frontend diff --git a/.github/actions/build-docker-image/action.yml b/.github/actions/build-docker-image/action.yml index 378f6bdb01b9..02a05d9605a7 100644 --- a/.github/actions/build-docker-image/action.yml +++ b/.github/actions/build-docker-image/action.yml @@ -12,6 +12,9 @@ inputs: required: false password: required: false + deno-version: + required: true + type: string outputs: image-name: @@ -59,7 +62,7 @@ runs: fi; echo "Build ${{ inputs.release }} Docker image" - docker build -t $IMAGE_NAME . + docker build --build-arg DENO_VERSION=${{ inputs.deno-version }} -t $IMAGE_NAME . echo "image-name-base=${IMAGE_NAME_BASE}" >> $GITHUB_OUTPUT echo "image-name=${IMAGE_NAME}" >> $GITHUB_OUTPUT diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 5af39b924057..ae84e376a0d9 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -9,6 +9,10 @@ inputs: required: true description: 'Node version' type: string + deno-version: + required: true + description: 'Deno version' + type: string platform: required: false description: 'Platform' @@ -66,6 +70,7 @@ runs: if: inputs.setup == 'true' with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ inputs.NPM_TOKEN }} @@ -79,6 +84,8 @@ runs: run: | args=(rocketchat ${{ inputs.build-containers }}) + export DENO_VERSION="${{ inputs.deno-version }}" + docker compose -f docker-compose-ci.yml build "${args[@]}" - name: Publish Docker images to GitHub Container Registry diff --git a/.github/actions/meteor-build/action.yml b/.github/actions/meteor-build/action.yml index 551a57d28a7c..bfd4ae7f5c20 100644 --- a/.github/actions/meteor-build/action.yml +++ b/.github/actions/meteor-build/action.yml @@ -16,6 +16,10 @@ inputs: NPM_TOKEN: required: false description: 'NPM token' + deno-version: + required: true + description: 'Deno version' + type: string runs: using: composite @@ -30,6 +34,7 @@ runs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ inputs.NPM_TOKEN }} diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml index 1035e2835792..120797d2ba3c 100644 --- a/.github/actions/setup-node/action.yml +++ b/.github/actions/setup-node/action.yml @@ -11,10 +11,11 @@ inputs: install: required: false description: 'Install dependencies' - deno-dir: - required: false - description: 'Deno directory' - default: ~/.deno-cache + type: boolean + deno-version: + required: true + description: 'Deno version' + type: string NPM_TOKEN: required: false description: 'NPM token' @@ -28,21 +29,20 @@ runs: using: composite steps: - - run: echo 'DENO_DIR=${{ inputs.deno-dir }}' >> $GITHUB_ENV - shell: bash - - - name: Cache Node Modules + - name: Cache Node Modules & Deno if: inputs.cache-modules id: cache-node-modules uses: actions/cache@v3 with: + # We need to cache node_modules for all workspaces with "hoistingLimits" defined path: | .turbo/cache node_modules - ${{ env.DENO_DIR }} apps/meteor/node_modules apps/meteor/ee/server/services/node_modules - key: node-modules-${{ hashFiles('yarn.lock') }} + packages/apps-engine/node_modules + packages/apps-engine/.deno-cache + key: node-modules-${{ hashFiles('yarn.lock') }}-deno-${{ hashFiles('packages/apps-engine/deno-runtime/deno.lock') }} # # Could use this command to list all paths to save: # find . -name 'node_modules' -prune | grep -v "/\.meteor/" | grep -v "/meteor/packages/" @@ -54,6 +54,11 @@ runs: node-version: ${{ inputs.node-version }} cache: 'yarn' + - name: Use Deno ${{ inputs.deno-version }} + uses: denoland/setup-deno@v1 + with: + deno-version: ${{ inputs.deno-version }} + - name: yarn login shell: bash if: inputs.NPM_TOKEN diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index af50b3230ba7..41facad89a03 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + deno-version: + required: true + type: string env: TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} @@ -33,6 +36,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci-deploy-gh-pages-preview.yml b/.github/workflows/ci-deploy-gh-pages-preview.yml index 17f247ddad94..8a0905a174bb 100644 --- a/.github/workflows/ci-deploy-gh-pages-preview.yml +++ b/.github/workflows/ci-deploy-gh-pages-preview.yml @@ -23,6 +23,7 @@ jobs: if: github.event.action != 'closed' with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-deploy-gh-pages.yml b/.github/workflows/ci-deploy-gh-pages.yml index b381e05ae5d8..0aab8022c7e6 100644 --- a/.github/workflows/ci-deploy-gh-pages.yml +++ b/.github/workflows/ci-deploy-gh-pages.yml @@ -18,6 +18,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index a80a40419e9f..f219f39c0614 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + deno-version: + required: true + type: string lowercase-repo: required: true type: string @@ -128,6 +131,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 840808ff5e31..883212d0cf3d 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -6,6 +6,9 @@ on: node-version: required: true type: string + deno-version: + required: true + type: string enterprise-license: required: false type: string @@ -37,6 +40,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ inputs.node-version }} + deno-version: ${{ inputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40260f71d21f..6b6fa426ca96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: rc-dockerfile-alpine: '${{ github.workspace }}/apps/meteor/.docker/Dockerfile.alpine' rc-docker-tag-alpine: '${{ steps.docker.outputs.gh-docker-tag }}.alpine' node-version: ${{ steps.var.outputs.node-version }} + deno-version: ${{ steps.var.outputs.deno-version }} # this is 100% intentional, secrets are not available for forks, so ee-tests will always fail # to avoid this, we are using a dummy license, expiring at 2025-06-31 enterprise-license: X/XumwIkgwQuld0alWKt37lVA90XjKOrfiMvMZ0/RtqsMtrdL9GoAk+4jXnaY1b2ePoG7XSzGhuxEDxFKIWJK3hIKGNTvrd980LgH5sM5+1T4P42ivSpd8UZi0bwjJkCFLIu9RozzYwslGG0IehMxe0S6VjcO0UYlUJtbMCBHuR2WmTAmO6YVU3ln+pZCbrPFaTPSS1RovhKaNCNkZwIx/CLWW8UTXUuFV/ML4PbKKVoa5nvvJwPeatgL7UCnlSD90lfCiiuikpzj/Y/JLkIL6velFbwNxsrxg9iRJ2k0sKheMMSmlTiGzSvZUm+na5WQq91aKGncih+DmaEZA7QGrjp4eoA0dqTk6OmItsy0fHmQhvZIOKNMeO7vNQiLbaSV6rqibrzu7WPpeIvsvL57T1h37USoCSB6+jDqkzdfoqIpz8BxTiJDj1d8xGPJFVrgxoqQqkj9qIP/gCaEz5DF39QFv5sovk4yK2O8fEQYod2d14V9yECYl4szZPMk1IBfCAC2w7czWGHHFonhL+CQGT403y5wmDmnsnjlCqMKF72odqfTPTI8XnCvJDriPMWohnQEAGtTTyciAhNokx/mjAVJ4NeZPcsbm4BjhvJvnjxx/BhYhBBTNWPaCSZzocfrGUj9Z+ZA7BEz+xAFQyGDx3xRzqIXfT0G7w8fvgYJMU= @@ -39,6 +40,7 @@ jobs: with: sparse-checkout: | package.json + .tool-versions sparse-checkout-cone-mode: false ref: ${{ github.ref }} @@ -53,6 +55,10 @@ jobs: echo "NODE_VERSION: ${NODE_VERSION}" echo "node-version=${NODE_VERSION}" >> $GITHUB_OUTPUT + DENO_VERSION=$(awk '$1=="deno"{ print $2 }' .tool-versions) + echo "DENO_VERSION: ${DENO_VERSION}" + echo "deno-version=${DENO_VERSION}" >> $GITHUB_OUTPUT + - id: by-tag run: | if echo "$GITHUB_REF_NAME" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' ; then @@ -150,6 +156,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -194,6 +201,7 @@ jobs: - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} coverage: ${{ github.event_name != 'release' }} build-prod: @@ -224,6 +232,7 @@ jobs: - uses: ./.github/actions/meteor-build with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} coverage: ${{ github.event_name != 'release' }} build-gh-docker-coverage: @@ -252,6 +261,7 @@ jobs: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -280,6 +290,7 @@ jobs: CR_USER: ${{ secrets.CR_USER }} CR_PAT: ${{ secrets.CR_PAT }} node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} platform: ${{ matrix.platform }} build-containers: ${{ matrix.platform == 'alpine' && 'authorization-service account-service ddp-streamer-service presence-service stream-hub-service queue-worker-service omnichannel-transcript-service' || '' }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -300,6 +311,7 @@ jobs: uses: ./.github/workflows/ci-code-check.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} test-unit: name: 🔨 Test Unit @@ -308,6 +320,7 @@ jobs: uses: ./.github/workflows/ci-test-unit.yml with: node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -321,6 +334,7 @@ jobs: type: api release: ce node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -344,6 +358,7 @@ jobs: shard: '[1, 2, 3, 4]' total-shard: 4 node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -371,6 +386,7 @@ jobs: enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} mongodb-version: "['4.4']" node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -395,6 +411,7 @@ jobs: total-shard: 5 mongodb-version: "['4.4']" node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -425,6 +442,7 @@ jobs: total-shard: 5 mongodb-version: "['6.0']" node-version: ${{ needs.release-versions.outputs.node-version }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} rc-dockerfile: ${{ needs.release-versions.outputs.rc-dockerfile }} rc-docker-tag: ${{ needs.release-versions.outputs.rc-docker-tag }} @@ -564,6 +582,7 @@ jobs: username: ${{ secrets.CR_USER }} password: ${{ secrets.CR_PAT }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + deno-version: ${{ needs.release-versions.outputs.deno-version }} docker-image-publish: name: 🚀 Publish Docker Image (main) diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index b2eae5d90b92..70e9eb354a06 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -35,6 +35,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/pr-update-description.yml b/.github/workflows/pr-update-description.yml index 084f2a383480..26ffffc6c86f 100644 --- a/.github/workflows/pr-update-description.yml +++ b/.github/workflows/pr-update-description.yml @@ -22,6 +22,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 3f2067ac7ec3..fe049f6a8369 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -25,6 +25,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 4a1e67fca33a..8c9048710dd2 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -16,6 +16,7 @@ jobs: uses: ./.github/actions/setup-node with: node-version: 14.21.3 + deno-version: 1.37.1 cache-modules: true install: true NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000000..bc89cc40f83e --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +deno 1.37.1 diff --git a/README.md b/README.md index 6461ad602516..564ca75d2b11 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ You can follow these instructions to setup a dev environment: - Install **Node 14.x (LTS)** either [manually](https://nodejs.org/dist/latest-v14.x/) or using a tool like [nvm](https://github.com/creationix/nvm) or [volta](https://volta.sh/) (recommended) - Install **Meteor** ([version here](apps/meteor/.meteor/release)): https://docs.meteor.com/about/install.html - Install **yarn**: https://yarnpkg.com/getting-started/install +- Install **Deno 1.x**: https://docs.deno.com/runtime/fundamentals/installation/ - Clone this repo: `git clone https://github.com/RocketChat/Rocket.Chat.git` - Run `yarn` to install dependencies diff --git a/apps/meteor/.docker/Dockerfile b/apps/meteor/.docker/Dockerfile index 1e9ed3f5e592..75df2cb90678 100644 --- a/apps/meteor/.docker/Dockerfile +++ b/apps/meteor/.docker/Dockerfile @@ -1,3 +1,7 @@ +ARG DENO_VERSION="1.37.1" + +FROM denoland/deno:bin-${DENO_VERSION} as deno + FROM node:14.21.3-bullseye-slim LABEL maintainer="buildmaster@rocket.chat" @@ -10,6 +14,8 @@ RUN groupadd -g 65533 -r rocketchat \ && apt-get update \ && apt-get install -y --no-install-recommends fontconfig +COPY --from=deno /deno /bin/deno + # --chown requires Docker 17.12 and works only on Linux ADD --chown=rocketchat:rocketchat . /app @@ -20,8 +26,7 @@ ENV DEPLOY_METHOD=docker \ HOME=/tmp \ PORT=3000 \ ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads \ - DENO_DIR=/usr/share/deno + Accounts_AvatarStorePath=/app/uploads RUN aptMark="$(apt-mark showmanual)" \ && apt-get install -y --no-install-recommends g++ make python3 ca-certificates \ @@ -29,8 +34,6 @@ RUN aptMark="$(apt-mark showmanual)" \ && npm install \ && cd npm/node_modules/isolated-vm \ && npm install \ - && cd /app/bundle/programs/server/npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ - && ../../../deno-bin/bin/deno cache main.ts \ && apt-mark auto '.*' > /dev/null \ && apt-mark manual $aptMark > /dev/null \ && find /usr/local -type f -executable -exec ldd '{}' ';' \ diff --git a/apps/meteor/.docker/Dockerfile.alpine b/apps/meteor/.docker/Dockerfile.alpine index feebf76a03e7..aaa2d2552ab3 100644 --- a/apps/meteor/.docker/Dockerfile.alpine +++ b/apps/meteor/.docker/Dockerfile.alpine @@ -1,7 +1,17 @@ +ARG DENO_VERSION="1.37.1" + +FROM denoland/deno:bin-${DENO_VERSION} as deno + FROM node:14.21.3-alpine3.16 +LABEL maintainer="buildmaster@rocket.chat" + ENV LANG=C.UTF-8 +## Alpine 3.16 does not have a deno package, but newer versions have it +## So as soon as we can update the Alpine version, we can replace the following +## GLIBC installation part by an `apk add deno` + # Installing glibc deps required by Deno # This replaces libc6-compat # Copied from https://github.com/Docker-Hub-frolvlad/docker-alpine-glibc, which denoland/deno:alpine-1.37.1 uses @@ -11,7 +21,7 @@ RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ - apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \ + apk add --no-cache --virtual=.build-dependencies wget ca-certificates ttf-dejavu && \ echo \ "-----BEGIN PUBLIC KEY-----\ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\ @@ -44,12 +54,11 @@ RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases rm \ "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ - "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ - apk add --no-cache ttf-dejavu + "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" -ADD . /app +COPY --from=deno /deno /bin/deno -LABEL maintainer="buildmaster@rocket.chat" +ADD . /app # needs a mongo instance - defaults to container linking with alias 'mongo' ENV DEPLOY_METHOD=docker \ @@ -58,13 +67,12 @@ ENV DEPLOY_METHOD=docker \ HOME=/tmp \ PORT=3000 \ ROOT_URL=http://localhost:3000 \ - Accounts_AvatarStorePath=/app/uploads \ - DENO_DIR=/usr/share/deno + Accounts_AvatarStorePath=/app/uploads RUN set -x \ && apk add --no-cache --virtual .fetch-deps python3 make g++ \ && cd /app/bundle/programs/server \ - && npm install --production \ + && npm install --omit=dev --unsafe-perm \ # Start hack for sharp... && rm -rf npm/node_modules/sharp \ && npm install sharp@0.32.6 \ @@ -75,9 +83,6 @@ RUN set -x \ && npm install isolated-vm@4.4.2 \ && mv node_modules/isolated-vm npm/node_modules/isolated-vm \ # End hack for isolated-vm - # Cache Deno dependencies for Apps-Engine - && cd npm/node_modules/@rocket.chat/apps-engine/deno-runtime \ - && /app/bundle/programs/server/npm/node_modules/deno-bin/bin/deno cache main.ts \ && cd /app/bundle/programs/server/npm \ && npm rebuild bcrypt --build-from-source \ && npm cache clear --force \ diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 390b2c646cd1..9eb2e917819d 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -18,7 +18,7 @@ "author": "Rocket.Chat", "license": "MIT", "dependencies": { - "@rocket.chat/apps-engine": "1.45.0-alpha.868", + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 3767939a7e3c..b5c9003b6974 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -230,7 +230,7 @@ "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps": "workspace:^", - "@rocket.chat/apps-engine": "1.45.0-alpha.868", + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/base64": "workspace:^", "@rocket.chat/cas-validate": "workspace:^", "@rocket.chat/core-services": "workspace:^", diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 23ee3b125524..0fe101c8fab1 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -8,6 +8,8 @@ services: build: dockerfile: ${RC_DOCKERFILE} context: /tmp/build + args: + DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${RC_DOCKER_TAG} environment: - TEST_MODE=true @@ -39,6 +41,7 @@ services: context: . args: SERVICE: authorization-service + DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG} environment: - 'MONGO_URL=${MONGO_URL}' @@ -73,6 +76,7 @@ services: context: . args: SERVICE: presence-service + DENO_VERSION: ${DENO_VERSION} image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG} environment: - MONGO_URL=${MONGO_URL} diff --git a/ee/apps/account-service/Dockerfile b/ee/apps/account-service/Dockerfile index c80d4f2eb376..a97290a43394 100644 --- a/ee/apps/account-service/Dockerfile +++ b/ee/apps/account-service/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/authorization-service/Dockerfile b/ee/apps/authorization-service/Dockerfile index 9a9e8ded922c..9ddeadd380fe 100644 --- a/ee/apps/authorization-service/Dockerfile +++ b/ee/apps/authorization-service/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/ddp-streamer/Dockerfile b/ee/apps/ddp-streamer/Dockerfile index 32103dc3528b..dea2bc3790a1 100644 --- a/ee/apps/ddp-streamer/Dockerfile +++ b/ee/apps/ddp-streamer/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 36c244f41180..f250b9e33106 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -15,7 +15,6 @@ ], "author": "Rocket.Chat", "dependencies": { - "@rocket.chat/apps-engine": "1.45.0-alpha.868", "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "~0.31.25", @@ -45,6 +44,7 @@ "ws": "^8.8.1" }, "devDependencies": { + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/ddp-client": "workspace:~", "@rocket.chat/eslint-config": "workspace:^", "@types/ejson": "^2.2.2", diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile index 9b7e47968e68..0f18534e1453 100644 --- a/ee/apps/omnichannel-transcript/Dockerfile +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/presence-service/Dockerfile b/ee/apps/presence-service/Dockerfile index 430880d29606..78c6a98f809a 100644 --- a/ee/apps/presence-service/Dockerfile +++ b/ee/apps/presence-service/Dockerfile @@ -13,6 +13,10 @@ COPY ./packages/agenda/dist packages/agenda/dist COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/core-typings/package.json packages/core-typings/package.json COPY ./packages/core-typings/dist packages/core-typings/dist diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile index 9b7e47968e68..0f18534e1453 100644 --- a/ee/apps/queue-worker/Dockerfile +++ b/ee/apps/queue-worker/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/apps/stream-hub-service/Dockerfile b/ee/apps/stream-hub-service/Dockerfile index 9a9e8ded922c..9ddeadd380fe 100644 --- a/ee/apps/stream-hub-service/Dockerfile +++ b/ee/apps/stream-hub-service/Dockerfile @@ -7,6 +7,10 @@ WORKDIR /app COPY ./packages/core-services/package.json packages/core-services/package.json COPY ./packages/core-services/dist packages/core-services/dist +COPY ./packages/apps-engine/package.json packages/apps-engine/package.json +COPY ./packages/apps-engine/client packages/apps-engine/client +COPY ./packages/apps-engine/definition packages/apps-engine/definition + COPY ./packages/agenda/package.json packages/agenda/package.json COPY ./packages/agenda/dist packages/agenda/dist diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 912b4bf453fd..06bbc244fd48 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -6,7 +6,7 @@ "@babel/core": "~7.22.20", "@babel/preset-env": "~7.22.20", "@babel/preset-typescript": "~7.22.15", - "@rocket.chat/apps-engine": "1.45.0-alpha.868", + "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "^14.18.63", diff --git a/packages/apps-engine/.eslintignore b/packages/apps-engine/.eslintignore new file mode 100644 index 000000000000..f7e4e0b38e59 --- /dev/null +++ b/packages/apps-engine/.eslintignore @@ -0,0 +1,8 @@ +!/gulpfile.js +/client +/definition +/docs +/server +/lib +/deno-runtime +/.deno diff --git a/packages/apps-engine/.eslintrc.json b/packages/apps-engine/.eslintrc.json new file mode 100644 index 000000000000..8e42d7cfa9f6 --- /dev/null +++ b/packages/apps-engine/.eslintrc.json @@ -0,0 +1,58 @@ +{ + "extends": "@rocket.chat/eslint-config", + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig-lint.json" + }, + "rules": { + "@typescript-eslint/ban-types": [ + "error", + { + "types": { + "{}": false + } + } + ], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": ["function", "parameter", "variable"], + "modifiers": ["destructured"], + "format": null + }, + { + "selector": ["variable"], + "format": ["camelCase", "UPPER_CASE", "PascalCase"], + "leadingUnderscore": "allowSingleOrDouble" + }, + { + "selector": ["function"], + "format": ["camelCase", "PascalCase"], + "leadingUnderscore": "allowSingleOrDouble" + }, + { + "selector": ["parameter"], + "format": ["camelCase"], + "leadingUnderscore": "allow" + }, + { + "selector": ["parameter"], + "format": ["camelCase"], + "modifiers": ["unused"], + "leadingUnderscore": "allow" + }, + { + "selector": ["interface"], + "format": ["PascalCase"], + "custom": { + "regex": "^I[A-Z]", + "match": true + } + } + ], + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], + "new-cap": "off", + "no-await-in-loop": "off" + } +} diff --git a/packages/apps-engine/.gitignore b/packages/apps-engine/.gitignore new file mode 100644 index 000000000000..c4568f9c7430 --- /dev/null +++ b/packages/apps-engine/.gitignore @@ -0,0 +1,61 @@ +# Created by https://www.gitignore.io/api/node + +### Node ### +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +.deno +.deno-cache + +# Optional REPL history +.node_repl_history + +### Typings ### +## Ignore downloaded typings +/typings + +## dev environment stuff +/examples/ +/dev-dist/ +/data/ +/tests/test-data/dbs +/client +/definition +/server +/lib + +.DS_Store +.idea/ diff --git a/packages/apps-engine/.prettierrc b/packages/apps-engine/.prettierrc new file mode 100644 index 000000000000..9b77117b35c0 --- /dev/null +++ b/packages/apps-engine/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 160 +} diff --git a/packages/apps-engine/README.md b/packages/apps-engine/README.md new file mode 100644 index 000000000000..d73e09f1aed7 --- /dev/null +++ b/packages/apps-engine/README.md @@ -0,0 +1,125 @@ +## Thoughts While Working (for docs) +- Apps which don't provide a valid uuid4 id will be assigned one, but this is not recommended and your App should provide an id +- The language strings are only done on the clients (`TAPi18next.addResourceBundle(lang, projectName, translations);`) +- The implementer of this should restrict the server setting access and environmental variables. Idea is to allow the implementer to have a default set of restricted ones while letting the admin/owner of the server to restrict it even further or lift the restriction on some more. Simple interface with settings and checkbox to allow/disallow them. :thinking: + +## What does the Apps-Engine enable you to do? +The Apps-Engine is Rocket.Chat's _plugin framework_ - it provides the APIs for Rocket.Chat Apps to interact with the host system. + +Currently, a Rocket.Chat App can: +- Listen to message events + - before/after sent + - before/after updated + - before/after deleted +- Listen to room events + - before/after created + - before/after deleted +- Send messages to users and livechat visitors +- Register new slash commands +- Register new HTTP endpoints + +Some features the Engine allows Apps to use: +- Key-Value Storage system +- App specific settings + +## Development environment with Rocket.Chat +When developing new functionalities, you need to integrate the local version of the Apps-Engine with your local version of Rocket.Chat. + +First of all, make sure you've installed all required packages and compiled the changes you've made to the Apps-Engine, since that is what Rocket.Chat will execute: +```sh +npm install +npm run compile +``` + +Now, you need to setup a local Rocket.Chat server, [so head to the project's README for instructions on getting started](https://github.com/RocketChat/Rocket.Chat#development) (if you haven't already). Make sure to actually clone the repo, since you will probably need to add some code to it in order to make your new functionality work. + +After that, `cd` into Rocket.Chat folder and run: +```sh +meteor npm install PATH_TO_APPS_ENGINE +``` + +Where `PATH_TO_APPS_ENGINE` is the path to the Apps-Engine repo you've cloned. + +That's it! Now when you start Rocket.Chat with the `meteor` command, it will use your local Apps-Engine instead of the one on NPM :) + +Whenever you make changes to the engine, run `npm run compile` again - meteor will take care of restarting the server due to the changes. + +## Troubleshooting +1. Sometimes, when you update the Apps-Engine code and compile it while Rocket.Chat is running, you might run on errors similar to these: + +``` +Unable to resolve some modules: + + "@rocket.chat/apps-engine/definition/AppStatus" in +/Users/dev/rocket.chat/Rocket.Chat/app/apps/client/admin/helpers.js (web.browser) + +If you notice problems related to these missing modules, consider running: + + meteor npm install --save @rocket.chat/apps-engine +``` + +Simply restart the meteor process and it should be fixed. + +2. Sometimes when using `meteor npm install PATH_TO_APPS_ENGINE` will cause the following error :- + +``` +npm ERR! code ENOENT +npm ERR! syscall rename +npm ERR! path PATH_TO_ROCKETCHAT/node_modules/.staging/@rocket.chat/apps-engine-c7135600/node_modules/@babel/code-frame +npm ERR! dest PATH_TO_ROCKETCHAT/node_modules/.staging/@babel/code-frame-f3697825 +npm ERR! errno -2 +npm ERR! enoent ENOENT: no such file or directory, rename 'PATH_TO_ROCKETCHAT/node_modules/.staging/@rocket.chat/apps-engine-c7135600/node_modules/@babel/code-frame' -> 'PATH_TO_ROCKETCHAT/node_modules/.staging/@babel/code-frame-f3697825' +npm ERR! enoent This is related to npm not being able to find a file. +npm ERR! enoent +``` +Here `PATH_TO_ROCKETCHAT` is the path to the main rocketchat server repo in your system +To correct this we reinstall the package once again deleting the previous package +``` +~/Rocket.Chat$ rm -rf node_modules/@rocket.chat/apps-engine +~/Rocket.Chat$ cd PATH_TO_APP_ENGINE +~/Rocket.Chat.Apps-engine$ npm install +~/Rocket.Chat.Apps-engine$ cd PATH_TO_ROCKETCHAT +~/Rocket.Chat$ meteor npm install ../Rocket.Chat.Apps-engine +``` + +## Implementer Needs to Implement: +- `src/server/storage/AppStorage` +- `src/server/storage/AppLogStorage` +- `src/server/bridges/*` + +## Testing Framework: +Makes great usage of TypeScript and decorators: https://github.com/alsatian-test/alsatian/wiki +* To run the tests do: `npm run unit-tests` +* To generate the coverage information: `npm run check-coverage` +* To view the coverage: `npm run view-coverage` + +# Rocket.Chat Apps TypeScript Definitions + +## Handlers +Handlers are essentially "listeners" for different events, except there are various ways to handle an event. +When something happens there is `pre` and `post` handlers. +The set of `pre` handlers happens before the event is finalized. +The set of `post` handlers happens after the event is finalized. +With that said, the rule of thumb is that if you are going to modify, extend, or change the data backing the event then that should be done in the `pre` handlers. If you are simply wanting to listen for when something happens and not modify anything, then the `post` is the way to go. + +The order in which they happen is: +* Pre**Event**Prevent +* Pre**Event**Extend +* Pre**Event**Modify +* Post**Event** + +Here is an explanation of what each of them means: +* **Prevent**: This is ran to determine whether the event should be prevented or not. +* **Extend**: This is ran to allow extending the data without being destructive of the data (adding an attachment to a message for example). +* **Modify**: This is ran and allows for destructive changes to the data (change any and everything). +* Post**Event**: Is mostly for simple listening and no changes can be made to the data. + +## Generating/Updating Documentation +To update or generate the documentation, please commit your changes first and then in a second commit provide the updated documentation. + +# Engage with us +## Share your story +We’d love to hear about [your experience](https://survey.zohopublic.com/zs/e4BUFG) and potentially feature it on our [Blog](https://rocket.chat/case-studies/?utm_source=github&utm_medium=readme&utm_campaign=community). + +## Subscribe for Updates +Once a month our marketing team releases an email update with news about product releases, company related topics, events and use cases. [Sign Up!](https://rocket.chat/newsletter/?utm_source=github&utm_medium=readme&utm_campaign=community) diff --git a/packages/apps-engine/deno-runtime/.gitignore b/packages/apps-engine/deno-runtime/.gitignore new file mode 100644 index 000000000000..5942ea3a153e --- /dev/null +++ b/packages/apps-engine/deno-runtime/.gitignore @@ -0,0 +1 @@ +.deno/ diff --git a/packages/apps-engine/deno-runtime/AppObjectRegistry.ts b/packages/apps-engine/deno-runtime/AppObjectRegistry.ts new file mode 100644 index 000000000000..9069c17eaac5 --- /dev/null +++ b/packages/apps-engine/deno-runtime/AppObjectRegistry.ts @@ -0,0 +1,26 @@ +export type Maybe = T | null | undefined; + +export const AppObjectRegistry = new class { + registry: Record = {}; + + public get(key: string): Maybe { + return this.registry[key] as Maybe; + } + + public set(key: string, value: unknown): void { + this.registry[key] = value; + } + + public has(key: string): boolean { + return key in this.registry; + } + + public delete(key: string): void { + delete this.registry[key]; + } + + public clear(): void { + this.registry = {}; + } +} + diff --git a/packages/apps-engine/deno-runtime/acorn-walk.d.ts b/packages/apps-engine/deno-runtime/acorn-walk.d.ts new file mode 100644 index 000000000000..25861f3bce0f --- /dev/null +++ b/packages/apps-engine/deno-runtime/acorn-walk.d.ts @@ -0,0 +1,170 @@ +import type acorn from "./acorn.d.ts"; + +export type FullWalkerCallback = ( + node: acorn.AnyNode, + state: TState, + type: string +) => void + +export type FullAncestorWalkerCallback = ( + node: acorn.AnyNode, + state: TState, + ancestors: acorn.AnyNode[], + type: string +) => void + +type AggregateType = { + Expression: acorn.Expression, + Statement: acorn.Statement, + Pattern: acorn.Pattern, + ForInit: acorn.VariableDeclaration | acorn.Expression +} + +export type SimpleVisitors = { + [type in acorn.AnyNode["type"]]?: (node: Extract, state: TState) => void +} & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState) => void +} + +export type AncestorVisitors = { + [type in acorn.AnyNode["type"]]?: ( node: Extract, state: TState, ancestors: acorn.Node[] +) => void +} & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, ancestors: acorn.Node[]) => void +} + +export type WalkerCallback = (node: acorn.Node, state: TState) => void + +export type RecursiveVisitors = { + [type in acorn.AnyNode["type"]]?: ( node: Extract, state: TState, callback: WalkerCallback) => void +} & { + [type in keyof AggregateType]?: (node: AggregateType[type], state: TState, callback: WalkerCallback) => void +} + +export type FindPredicate = (type: string, node: acorn.Node) => boolean + +export interface Found { + node: acorn.Node, + state: TState +} + +/** + * does a 'simple' walk over a tree + * @param node the AST node to walk + * @param visitors an object with properties whose names correspond to node types in the {@link https://github.com/estree/estree | ESTree spec}. The properties should contain functions that will be called with the node object and, if applicable the state at that point. + * @param base a walker algorithm + * @param state a start state. The default walker will simply visit all statements and expressions and not produce a meaningful state. (An example of a use of state is to track scope at each point in the tree.) + */ +export function simple( + node: acorn.Node, + visitors: SimpleVisitors, + base?: RecursiveVisitors, + state?: TState +): void + +/** + * does a 'simple' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. + * @param node + * @param visitors + * @param base + * @param state + */ +export function ancestor( + node: acorn.Node, + visitors: AncestorVisitors, + base?: RecursiveVisitors, + state?: TState + ): void + +/** + * does a 'recursive' walk, where the walker functions are responsible for continuing the walk on the child nodes of their target node. + * @param node + * @param state the start state + * @param functions contain an object that maps node types to walker functions + * @param base provides the fallback walker functions for node types that aren't handled in the {@link functions} object. If not given, the default walkers will be used. + */ +export function recursive( + node: acorn.Node, + state: TState, + functions: RecursiveVisitors, + base?: RecursiveVisitors +): void + +/** + * does a 'full' walk over a tree, calling the {@link callback} with the arguments (node, state, type) for each node + * @param node + * @param callback + * @param base + * @param state + */ +export function full( + node: acorn.Node, + callback: FullWalkerCallback, + base?: RecursiveVisitors, + state?: TState +): void + +/** + * does a 'full' walk over a tree, building up an array of ancestor nodes (including the current node) and passing the array to the callbacks as a third parameter. + * @param node + * @param callback + * @param base + * @param state + */ +export function fullAncestor( + node: acorn.AnyNode, + callback: FullAncestorWalkerCallback, + base?: RecursiveVisitors, + state?: TState +): void + +/** + * builds a new walker object by using the walker functions in {@link functions} and filling in the missing ones by taking defaults from {@link base}. + * @param functions + * @param base + */ +export function make( + functions: RecursiveVisitors, + base?: RecursiveVisitors +): RecursiveVisitors + +/** + * tries to locate a node in a tree at the given start and/or end offsets, which satisfies the predicate test. {@link start} and {@link end} can be either `null` (as wildcard) or a `number`. {@link test} may be a string (indicating a node type) or a function that takes (nodeType, node) arguments and returns a boolean indicating whether this node is interesting. {@link base} and {@link state} are optional, and can be used to specify a custom walker. Nodes are tested from inner to outer, so if two nodes match the boundaries, the inner one will be preferred. + * @param node + * @param start + * @param end + * @param type + * @param base + * @param state + */ +export function findNodeAt( + node: acorn.AnyNode, + start: number | undefined, + end?: number | undefined, + type?: FindPredicate | string, + base?: RecursiveVisitors, + state?: TState +): Found | undefined + +/** + * like {@link findNodeAt}, but will match any node that exists 'around' (spanning) the given position. + * @param node + * @param start + * @param type + * @param base + * @param state + */ +export function findNodeAround( + node: acorn.AnyNode, + start: number | undefined, + type?: FindPredicate | string, + base?: RecursiveVisitors, + state?: TState +): Found | undefined + +/** + * similar to {@link findNodeAround}, but will match all nodes after the given position (testing outer nodes before inner nodes). + */ +export const findNodeAfter: typeof findNodeAround + +export const base: RecursiveVisitors diff --git a/packages/apps-engine/deno-runtime/acorn.d.ts b/packages/apps-engine/deno-runtime/acorn.d.ts new file mode 100644 index 000000000000..0b5bc6b407b2 --- /dev/null +++ b/packages/apps-engine/deno-runtime/acorn.d.ts @@ -0,0 +1,857 @@ +export interface Node { + start?: number + end?: number + type: string + range?: [number, number] + loc?: SourceLocation | null +} + +export interface SourceLocation { + source?: string | null + start: Position + end: Position +} + +export interface Position { + /** 1-based */ + line: number + /** 0-based */ + column: number +} + +export interface Identifier extends Node { + type: "Identifier" + name: string +} + +export interface Literal extends Node { + type: "Literal" + value?: string | boolean | null | number | RegExp | bigint + raw?: string + regex?: { + pattern: string + flags: string + } + bigint?: string +} + +export interface Program extends Node { + type: "Program" + body: Array + sourceType: "script" | "module" +} + +export interface Function extends Node { + id?: Identifier | null + params: Array + body: BlockStatement | Expression + generator: boolean + expression: boolean + async: boolean +} + +export interface ExpressionStatement extends Node { + type: "ExpressionStatement" + expression: Expression | Literal + directive?: string +} + +export interface BlockStatement extends Node { + type: "BlockStatement" + body: Array +} + +export interface EmptyStatement extends Node { + type: "EmptyStatement" +} + +export interface DebuggerStatement extends Node { + type: "DebuggerStatement" +} + +export interface WithStatement extends Node { + type: "WithStatement" + object: Expression + body: Statement +} + +export interface ReturnStatement extends Node { + type: "ReturnStatement" + argument?: Expression | null +} + +export interface LabeledStatement extends Node { + type: "LabeledStatement" + label: Identifier + body: Statement +} + +export interface BreakStatement extends Node { + type: "BreakStatement" + label?: Identifier | null +} + +export interface ContinueStatement extends Node { + type: "ContinueStatement" + label?: Identifier | null +} + +export interface IfStatement extends Node { + type: "IfStatement" + test: Expression + consequent: Statement + alternate?: Statement | null +} + +export interface SwitchStatement extends Node { + type: "SwitchStatement" + discriminant: Expression + cases: Array +} + +export interface SwitchCase extends Node { + type: "SwitchCase" + test?: Expression | null + consequent: Array +} + +export interface ThrowStatement extends Node { + type: "ThrowStatement" + argument: Expression +} + +export interface TryStatement extends Node { + type: "TryStatement" + block: BlockStatement + handler?: CatchClause | null + finalizer?: BlockStatement | null +} + +export interface CatchClause extends Node { + type: "CatchClause" + param?: Pattern | null + body: BlockStatement +} + +export interface WhileStatement extends Node { + type: "WhileStatement" + test: Expression + body: Statement +} + +export interface DoWhileStatement extends Node { + type: "DoWhileStatement" + body: Statement + test: Expression +} + +export interface ForStatement extends Node { + type: "ForStatement" + init?: VariableDeclaration | Expression | null + test?: Expression | null + update?: Expression | null + body: Statement +} + +export interface ForInStatement extends Node { + type: "ForInStatement" + left: VariableDeclaration | Pattern + right: Expression + body: Statement +} + +export interface FunctionDeclaration extends Function { + type: "FunctionDeclaration" + id: Identifier + body: BlockStatement +} + +export interface VariableDeclaration extends Node { + type: "VariableDeclaration" + declarations: Array + kind: "var" | "let" | "const" +} + +export interface VariableDeclarator extends Node { + type: "VariableDeclarator" + id: Pattern + init?: Expression | null +} + +export interface ThisExpression extends Node { + type: "ThisExpression" +} + +export interface ArrayExpression extends Node { + type: "ArrayExpression" + elements: Array +} + +export interface ObjectExpression extends Node { + type: "ObjectExpression" + properties: Array +} + +export interface Property extends Node { + type: "Property" + key: Expression + value: Expression + kind: "init" | "get" | "set" + method: boolean + shorthand: boolean + computed: boolean +} + +export interface FunctionExpression extends Function { + type: "FunctionExpression" + body: BlockStatement +} + +export interface UnaryExpression extends Node { + type: "UnaryExpression" + operator: UnaryOperator + prefix: boolean + argument: Expression +} + +export type UnaryOperator = "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" + +export interface UpdateExpression extends Node { + type: "UpdateExpression" + operator: UpdateOperator + argument: Expression + prefix: boolean +} + +export type UpdateOperator = "++" | "--" + +export interface BinaryExpression extends Node { + type: "BinaryExpression" + operator: BinaryOperator + left: Expression | PrivateIdentifier + right: Expression +} + +export type BinaryOperator = "==" | "!=" | "===" | "!==" | "<" | "<=" | ">" | ">=" | "<<" | ">>" | ">>>" | "+" | "-" | "*" | "/" | "%" | "|" | "^" | "&" | "in" | "instanceof" | "**" + +export interface AssignmentExpression extends Node { + type: "AssignmentExpression" + operator: AssignmentOperator + left: Pattern + right: Expression +} + +export type AssignmentOperator = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "<<=" | ">>=" | ">>>=" | "|=" | "^=" | "&=" | "**=" | "||=" | "&&=" | "??=" + +export interface LogicalExpression extends Node { + type: "LogicalExpression" + operator: LogicalOperator + left: Expression + right: Expression +} + +export type LogicalOperator = "||" | "&&" | "??" + +export interface MemberExpression extends Node { + type: "MemberExpression" + object: Expression | Super + property: Expression | PrivateIdentifier + computed: boolean + optional: boolean +} + +export interface ConditionalExpression extends Node { + type: "ConditionalExpression" + test: Expression + alternate: Expression + consequent: Expression +} + +export interface CallExpression extends Node { + type: "CallExpression" + callee: Expression | Super + arguments: Array + optional: boolean +} + +export interface NewExpression extends Node { + type: "NewExpression" + callee: Expression + arguments: Array +} + +export interface SequenceExpression extends Node { + type: "SequenceExpression" + expressions: Array +} + +export interface ForOfStatement extends Node { + type: "ForOfStatement" + left: VariableDeclaration | Pattern + right: Expression + body: Statement + await: boolean +} + +export interface Super extends Node { + type: "Super" +} + +export interface SpreadElement extends Node { + type: "SpreadElement" + argument: Expression +} + +export interface ArrowFunctionExpression extends Function { + type: "ArrowFunctionExpression" +} + +export interface YieldExpression extends Node { + type: "YieldExpression" + argument?: Expression | null + delegate: boolean +} + +export interface TemplateLiteral extends Node { + type: "TemplateLiteral" + quasis: Array + expressions: Array +} + +export interface TaggedTemplateExpression extends Node { + type: "TaggedTemplateExpression" + tag: Expression + quasi: TemplateLiteral +} + +export interface TemplateElement extends Node { + type: "TemplateElement" + tail: boolean + value: { + cooked?: string | null + raw: string + } +} + +export interface AssignmentProperty extends Node { + type: "Property" + key: Expression + value: Pattern + kind: "init" + method: false + shorthand: boolean + computed: boolean +} + +export interface ObjectPattern extends Node { + type: "ObjectPattern" + properties: Array +} + +export interface ArrayPattern extends Node { + type: "ArrayPattern" + elements: Array +} + +export interface RestElement extends Node { + type: "RestElement" + argument: Pattern +} + +export interface AssignmentPattern extends Node { + type: "AssignmentPattern" + left: Pattern + right: Expression +} + +export interface Class extends Node { + id?: Identifier | null + superClass?: Expression | null + body: ClassBody +} + +export interface ClassBody extends Node { + type: "ClassBody" + body: Array +} + +export interface MethodDefinition extends Node { + type: "MethodDefinition" + key: Expression | PrivateIdentifier + value: FunctionExpression + kind: "constructor" | "method" | "get" | "set" + computed: boolean + static: boolean +} + +export interface ClassDeclaration extends Class { + type: "ClassDeclaration" + id: Identifier +} + +export interface ClassExpression extends Class { + type: "ClassExpression" +} + +export interface MetaProperty extends Node { + type: "MetaProperty" + meta: Identifier + property: Identifier +} + +export interface ImportDeclaration extends Node { + type: "ImportDeclaration" + specifiers: Array + source: Literal +} + +export interface ImportSpecifier extends Node { + type: "ImportSpecifier" + imported: Identifier | Literal + local: Identifier +} + +export interface ImportDefaultSpecifier extends Node { + type: "ImportDefaultSpecifier" + local: Identifier +} + +export interface ImportNamespaceSpecifier extends Node { + type: "ImportNamespaceSpecifier" + local: Identifier +} + +export interface ExportNamedDeclaration extends Node { + type: "ExportNamedDeclaration" + declaration?: Declaration | null + specifiers: Array + source?: Literal | null +} + +export interface ExportSpecifier extends Node { + type: "ExportSpecifier" + exported: Identifier | Literal + local: Identifier | Literal +} + +export interface AnonymousFunctionDeclaration extends Function { + type: "FunctionDeclaration" + id: null + body: BlockStatement +} + +export interface AnonymousClassDeclaration extends Class { + type: "ClassDeclaration" + id: null +} + +export interface ExportDefaultDeclaration extends Node { + type: "ExportDefaultDeclaration" + declaration: AnonymousFunctionDeclaration | FunctionDeclaration | AnonymousClassDeclaration | ClassDeclaration | Expression +} + +export interface ExportAllDeclaration extends Node { + type: "ExportAllDeclaration" + source: Literal + exported?: Identifier | Literal | null +} + +export interface AwaitExpression extends Node { + type: "AwaitExpression" + argument: Expression +} + +export interface ChainExpression extends Node { + type: "ChainExpression" + expression: MemberExpression | CallExpression +} + +export interface ImportExpression extends Node { + type: "ImportExpression" + source: Expression +} + +export interface ParenthesizedExpression extends Node { + type: "ParenthesizedExpression" + expression: Expression +} + +export interface PropertyDefinition extends Node { + type: "PropertyDefinition" + key: Expression | PrivateIdentifier + value?: Expression | null + computed: boolean + static: boolean +} + +export interface PrivateIdentifier extends Node { + type: "PrivateIdentifier" + name: string +} + +export interface StaticBlock extends Node { + type: "StaticBlock" + body: Array +} + +export type Statement = +| ExpressionStatement +| BlockStatement +| EmptyStatement +| DebuggerStatement +| WithStatement +| ReturnStatement +| LabeledStatement +| BreakStatement +| ContinueStatement +| IfStatement +| SwitchStatement +| ThrowStatement +| TryStatement +| WhileStatement +| DoWhileStatement +| ForStatement +| ForInStatement +| ForOfStatement +| Declaration + +export type Declaration = +| FunctionDeclaration +| VariableDeclaration +| ClassDeclaration + +export type Expression = +| Identifier +| Literal +| ThisExpression +| ArrayExpression +| ObjectExpression +| FunctionExpression +| UnaryExpression +| UpdateExpression +| BinaryExpression +| AssignmentExpression +| LogicalExpression +| MemberExpression +| ConditionalExpression +| CallExpression +| NewExpression +| SequenceExpression +| ArrowFunctionExpression +| YieldExpression +| TemplateLiteral +| TaggedTemplateExpression +| ClassExpression +| MetaProperty +| AwaitExpression +| ChainExpression +| ImportExpression +| ParenthesizedExpression + +export type Pattern = +| Identifier +| MemberExpression +| ObjectPattern +| ArrayPattern +| RestElement +| AssignmentPattern + +export type ModuleDeclaration = +| ImportDeclaration +| ExportNamedDeclaration +| ExportDefaultDeclaration +| ExportAllDeclaration + +export type AnyNode = Statement | Expression | Declaration | ModuleDeclaration | Literal | Program | SwitchCase | CatchClause | Property | Super | SpreadElement | TemplateElement | AssignmentProperty | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | MetaProperty | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier | AnonymousFunctionDeclaration | AnonymousClassDeclaration | PropertyDefinition | PrivateIdentifier | StaticBlock | VariableDeclaration | VariableDeclarator + +export function parse(input: string, options: Options): Program + +export function parseExpressionAt(input: string, pos: number, options: Options): Expression + +export function tokenizer(input: string, options: Options): { + getToken(): Token + [Symbol.iterator](): Iterator +} + +export type ecmaVersion = 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | "latest" + +export interface Options { + /** + * `ecmaVersion` indicates the ECMAScript version to parse. Must be + * either 3, 5, 6 (or 2015), 7 (2016), 8 (2017), 9 (2018), 10 + * (2019), 11 (2020), 12 (2021), 13 (2022), 14 (2023), or `"latest"` + * (the latest version the library supports). This influences + * support for strict mode, the set of reserved words, and support + * for new syntax features. + */ + ecmaVersion: ecmaVersion + + /** + * `sourceType` indicates the mode the code should be parsed in. + * Can be either `"script"` or `"module"`. This influences global + * strict mode and parsing of `import` and `export` declarations. + */ + sourceType?: "script" | "module" + + /** + * a callback that will be called when a semicolon is automatically inserted. + * @param lastTokEnd the position of the comma as an offset + * @param lastTokEndLoc location if {@link locations} is enabled + */ + onInsertedSemicolon?: (lastTokEnd: number, lastTokEndLoc?: Position) => void + + /** + * similar to `onInsertedSemicolon`, but for trailing commas + * @param lastTokEnd the position of the comma as an offset + * @param lastTokEndLoc location if `locations` is enabled + */ + onTrailingComma?: (lastTokEnd: number, lastTokEndLoc?: Position) => void + + /** + * By default, reserved words are only enforced if ecmaVersion >= 5. + * Set `allowReserved` to a boolean value to explicitly turn this on + * an off. When this option has the value "never", reserved words + * and keywords can also not be used as property names. + */ + allowReserved?: boolean | "never" + + /** + * When enabled, a return at the top level is not considered an error. + */ + allowReturnOutsideFunction?: boolean + + /** + * When enabled, import/export statements are not constrained to + * appearing at the top of the program, and an import.meta expression + * in a script isn't considered an error. + */ + allowImportExportEverywhere?: boolean + + /** + * By default, `await` identifiers are allowed to appear at the top-level scope only if {@link ecmaVersion} >= 2022. + * When enabled, await identifiers are allowed to appear at the top-level scope, + * but they are still not allowed in non-async functions. + */ + allowAwaitOutsideFunction?: boolean + + /** + * When enabled, super identifiers are not constrained to + * appearing in methods and do not raise an error when they appear elsewhere. + */ + allowSuperOutsideMethod?: boolean + + /** + * When enabled, hashbang directive in the beginning of file is + * allowed and treated as a line comment. Enabled by default when + * {@link ecmaVersion} >= 2023. + */ + allowHashBang?: boolean + + /** + * By default, the parser will verify that private properties are + * only used in places where they are valid and have been declared. + * Set this to false to turn such checks off. + */ + checkPrivateFields?: boolean + + /** + * When `locations` is on, `loc` properties holding objects with + * `start` and `end` properties as {@link Position} objects will be attached to the + * nodes. + */ + locations?: boolean + + /** + * a callback that will cause Acorn to call that export function with object in the same + * format as tokens returned from `tokenizer().getToken()`. Note + * that you are not allowed to call the parser from the + * callback—that will corrupt its internal state. + */ + onToken?: ((token: Token) => void) | Token[] + + + /** + * This takes a export function or an array. + * + * When a export function is passed, Acorn will call that export function with `(block, text, start, + * end)` parameters whenever a comment is skipped. `block` is a + * boolean indicating whether this is a block (`/* *\/`) comment, + * `text` is the content of the comment, and `start` and `end` are + * character offsets that denote the start and end of the comment. + * When the {@link locations} option is on, two more parameters are + * passed, the full locations of {@link Position} export type of the start and + * end of the comments. + * + * When a array is passed, each found comment of {@link Comment} export type is pushed to the array. + * + * Note that you are not allowed to call the + * parser from the callback—that will corrupt its internal state. + */ + onComment?: (( + isBlock: boolean, text: string, start: number, end: number, startLoc?: Position, + endLoc?: Position + ) => void) | Comment[] + + /** + * Nodes have their start and end characters offsets recorded in + * `start` and `end` properties (directly on the node, rather than + * the `loc` object, which holds line/column data. To also add a + * [semi-standardized][range] `range` property holding a `[start, + * end]` array with the same numbers, set the `ranges` option to + * `true`. + */ + ranges?: boolean + + /** + * It is possible to parse multiple files into a single AST by + * passing the tree produced by parsing the first file as + * `program` option in subsequent parses. This will add the + * toplevel forms of the parsed file to the `Program` (top) node + * of an existing parse tree. + */ + program?: Node + + /** + * When {@link locations} is on, you can pass this to record the source + * file in every node's `loc` object. + */ + sourceFile?: string + + /** + * This value, if given, is stored in every node, whether {@link locations} is on or off. + */ + directSourceFile?: string + + /** + * When enabled, parenthesized expressions are represented by + * (non-standard) ParenthesizedExpression nodes + */ + preserveParens?: boolean +} + +export class Parser { + options: Options + input: string + + constructor(options: Options, input: string, startPos?: number) + parse(): Program + + static parse(input: string, options: Options): Program + static parseExpressionAt(input: string, pos: number, options: Options): Expression + static tokenizer(input: string, options: Options): { + getToken(): Token + [Symbol.iterator](): Iterator + } + static extend(...plugins: ((BaseParser: typeof Parser) => typeof Parser)[]): typeof Parser +} + +export const defaultOptions: Options + +export function getLineInfo(input: string, offset: number): Position + +export class TokenType { + label: string + keyword: string | undefined +} + +export const tokTypes: { + num: TokenType + regexp: TokenType + string: TokenType + name: TokenType + privateId: TokenType + eof: TokenType + + bracketL: TokenType + bracketR: TokenType + braceL: TokenType + braceR: TokenType + parenL: TokenType + parenR: TokenType + comma: TokenType + semi: TokenType + colon: TokenType + dot: TokenType + question: TokenType + questionDot: TokenType + arrow: TokenType + template: TokenType + invalidTemplate: TokenType + ellipsis: TokenType + backQuote: TokenType + dollarBraceL: TokenType + + eq: TokenType + assign: TokenType + incDec: TokenType + prefix: TokenType + logicalOR: TokenType + logicalAND: TokenType + bitwiseOR: TokenType + bitwiseXOR: TokenType + bitwiseAND: TokenType + equality: TokenType + relational: TokenType + bitShift: TokenType + plusMin: TokenType + modulo: TokenType + star: TokenType + slash: TokenType + starstar: TokenType + coalesce: TokenType + + _break: TokenType + _case: TokenType + _catch: TokenType + _continue: TokenType + _debugger: TokenType + _default: TokenType + _do: TokenType + _else: TokenType + _finally: TokenType + _for: TokenType + _function: TokenType + _if: TokenType + _return: TokenType + _switch: TokenType + _throw: TokenType + _try: TokenType + _var: TokenType + _const: TokenType + _while: TokenType + _with: TokenType + _new: TokenType + _this: TokenType + _super: TokenType + _class: TokenType + _extends: TokenType + _export: TokenType + _import: TokenType + _null: TokenType + _true: TokenType + _false: TokenType + _in: TokenType + _instanceof: TokenType + _typeof: TokenType + _void: TokenType + _delete: TokenType +} + +export interface Comment { + type: "Line" | "Block" + value: string + start: number + end: number + loc?: SourceLocation + range?: [number, number] +} + +export class Token { + type: TokenType + start: number + end: number + loc?: SourceLocation + range?: [number, number] +} + +export const version: string diff --git a/packages/apps-engine/deno-runtime/deno.jsonc b/packages/apps-engine/deno-runtime/deno.jsonc new file mode 100644 index 000000000000..231d0924237a --- /dev/null +++ b/packages/apps-engine/deno-runtime/deno.jsonc @@ -0,0 +1,16 @@ +{ + "imports": { + "@rocket.chat/apps-engine/": "./../src/", + "@rocket.chat/ui-kit": "npm:@rocket.chat/ui-kit@^0.31.22", + "@msgpack/msgpack": "npm:@msgpack/msgpack@3.0.0-beta2", + "acorn": "npm:acorn@8.10.0", + "acorn-walk": "npm:acorn-walk@8.2.0", + "astring": "npm:astring@1.8.6", + "jsonrpc-lite": "npm:jsonrpc-lite@2.2.0", + "stack-trace": "npm:stack-trace@0.0.10", + "uuid": "npm:uuid@8.3.2" + }, + "tasks": { + "test": "deno test --no-check --allow-read=../../../" + } +} diff --git a/packages/apps-engine/deno-runtime/deno.lock b/packages/apps-engine/deno-runtime/deno.lock new file mode 100644 index 000000000000..86cebf98f63a --- /dev/null +++ b/packages/apps-engine/deno-runtime/deno.lock @@ -0,0 +1,107 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "npm:@msgpack/msgpack@3.0.0-beta2": "npm:@msgpack/msgpack@3.0.0-beta2", + "npm:@rocket.chat/ui-kit@^0.31.22": "npm:@rocket.chat/ui-kit@0.31.25_@rocket.chat+icons@0.32.0", + "npm:acorn-walk@8.2.0": "npm:acorn-walk@8.2.0", + "npm:acorn@8.10.0": "npm:acorn@8.10.0", + "npm:astring@1.8.6": "npm:astring@1.8.6", + "npm:jsonrpc-lite@2.2.0": "npm:jsonrpc-lite@2.2.0", + "npm:stack-trace": "npm:stack-trace@0.0.10", + "npm:stack-trace@0.0.10": "npm:stack-trace@0.0.10", + "npm:uuid@8.3.2": "npm:uuid@8.3.2" + }, + "npm": { + "@msgpack/msgpack@3.0.0-beta2": { + "integrity": "sha512-y+l1PNV0XDyY8sM3YtuMLK5vE3/hkfId+Do8pLo/OPxfxuFAUwcGz3oiiUuV46/aBpwTzZ+mRWVMtlSKbradhw==", + "dependencies": {} + }, + "@rocket.chat/icons@0.32.0": { + "integrity": "sha512-7yhhELKNLb9kUtXCvau0V+iMXraV2bOsxcPjc/ZtLR5VeeIDTeaflqRWGtLroX6f3bE+J1n5qB5zi8A4YXuH2g==", + "dependencies": {} + }, + "@rocket.chat/ui-kit@0.31.25_@rocket.chat+icons@0.32.0": { + "integrity": "sha512-yTgTKDw9SMlJ6p8n0PDO6zSvox/nHYUrwCIvILQeAK6PvTrgSe/u9CvU7ATTYjnQiQ603yEGR6dxjF4euCGdNA==", + "dependencies": { + "@rocket.chat/icons": "@rocket.chat/icons@0.32.0" + } + }, + "acorn-walk@8.2.0": { + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dependencies": {} + }, + "acorn@8.10.0": { + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dependencies": {} + }, + "astring@1.8.6": { + "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", + "dependencies": {} + }, + "jsonrpc-lite@2.2.0": { + "integrity": "sha512-/cbbSxtZWs1O7R4tWqabrCM/t3N8qKUZMAg9IUqpPvUs6UyRvm6pCNYkskyKN/XU0UgffW+NY2ZRr8t0AknX7g==", + "dependencies": {} + }, + "stack-trace@0.0.10": { + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dependencies": {} + }, + "uuid@8.3.2": { + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dependencies": {} + } + } + }, + "remote": { + "https://deno.land/std@0.203.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.203.0/assert/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.203.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.203.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.203.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.203.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.203.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.203.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.203.0/assert/assert_false.ts": "0ccbcaae910f52c857192ff16ea08bda40fdc79de80846c206bfc061e8c851c6", + "https://deno.land/std@0.203.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.203.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.203.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.203.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.203.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.203.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.203.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.203.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.203.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.203.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.203.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.203.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.203.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.203.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.203.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.203.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.203.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.203.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.203.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.203.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.203.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.203.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.203.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.203.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.203.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", + "https://deno.land/std@0.203.0/testing/mock.ts": "6576b4aa55ee20b1990d656a78fff83599e190948c00e9f25a7f3ac5e9d6492d", + "https://deno.land/std@0.216.0/io/types.ts": "748bbb3ac96abda03594ef5a0db15ce5450dcc6c0d841c8906f8b10ac8d32c96", + "https://deno.land/std@0.216.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038" + }, + "workspace": { + "dependencies": [ + "npm:@msgpack/msgpack@3.0.0-beta2", + "npm:@rocket.chat/ui-kit@^0.31.22", + "npm:acorn-walk@8.2.0", + "npm:acorn@8.10.0", + "npm:astring@1.8.6", + "npm:jsonrpc-lite@2.2.0", + "npm:stack-trace@0.0.10", + "npm:uuid@8.3.2" + ] + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/api-handler.ts b/packages/apps-engine/deno-runtime/handlers/api-handler.ts new file mode 100644 index 000000000000..32d30e532fd3 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/api-handler.ts @@ -0,0 +1,46 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { IApiEndpoint } from '@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { Logger } from '../lib/logger.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; + +export default async function apiHandler(call: string, params: unknown): Promise { + const [, path, httpMethod] = call.split(':'); + + const endpoint = AppObjectRegistry.get(`api:${path}`); + const logger = AppObjectRegistry.get('logger'); + + if (!endpoint) { + return new JsonRpcError(`Endpoint ${path} not found`, -32000); + } + + const method = endpoint[httpMethod as keyof IApiEndpoint]; + + if (typeof method !== 'function') { + return new JsonRpcError(`${path}'s ${httpMethod} not exists`, -32000); + } + + const [request, endpointInfo] = params as Array; + + logger?.debug(`${path}'s ${call} is being executed...`, request); + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(endpoint, [ + request, + endpointInfo, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger?.debug(`${path}'s ${call} was successfully executed.`); + + return result; + } catch (e) { + logger?.debug(`${path}'s ${call} was unsuccessful.`); + return new JsonRpcError(e.message || "Internal server error", -32000); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/construct.ts b/packages/apps-engine/deno-runtime/handlers/app/construct.ts new file mode 100644 index 000000000000..798a83d0923c --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/construct.ts @@ -0,0 +1,126 @@ +import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { require } from '../../lib/require.ts'; +import { sanitizeDeprecatedUsage } from '../../lib/sanitizeDeprecatedUsage.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { Socket } from 'node:net'; + +const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring', 'fs']; +const ALLOWED_EXTERNAL_MODULES = ['uuid']; + + +function prepareEnvironment() { + // Deno does not behave equally to Node when it comes to piping content to a socket + // So we intervene here + const originalFinal = Socket.prototype._final; + Socket.prototype._final = function _final(cb) { + // Deno closes the readable stream in the Socket earlier than Node + // The exact reason for that is yet unknown, so we'll need to simply delay the execution + // which allows data to be read in a response + setTimeout(() => originalFinal.call(this, cb), 1); + }; +} + +// As the apps are bundled, the only times they will call require are +// 1. To require native modules +// 2. To require external npm packages we may provide +// 3. To require apps-engine files +function buildRequire(): (module: string) => unknown { + return (module: string): unknown => { + if (ALLOWED_NATIVE_MODULES.includes(module)) { + return require(`node:${module}`); + } + + if (ALLOWED_EXTERNAL_MODULES.includes(module)) { + return require(`npm:${module}`); + } + + if (module.startsWith('@rocket.chat/apps-engine')) { + // Our `require` function knows how to handle these + return require(module); + } + + throw new Error(`Module ${module} is not allowed`); + }; +} + +function wrapAppCode(code: string): (require: (module: string) => unknown) => Promise> { + return new Function( + 'require', + ` + const { Buffer } = require('buffer'); + const exports = {}; + const module = { exports }; + const _error = console.error.bind(console); + const _console = { + log: _error, + error: _error, + debug: _error, + info: _error, + warn: _error, + }; + + const result = (async (exports,module,require,Buffer,console,globalThis,Deno) => { + ${code}; + })(exports,module,require,Buffer,_console,undefined,undefined); + + return result.then(() => module.exports);`, + ) as (require: (module: string) => unknown) => Promise>; +} + +export default async function handleConstructApp(params: unknown): Promise { + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [appPackage] = params as [IParseAppPackageResult]; + + if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + prepareEnvironment(); + + AppObjectRegistry.set('id', appPackage.info.id); + const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]); + + const require = buildRequire(); + const exports = await wrapAppCode(source)(require); + + // This is the same naive logic we've been using in the App Compiler + // Applying the correct type here is quite difficult because of the dynamic nature of the code + // deno-lint-ignore no-explicit-any + const appClass = Object.values(exports)[0] as any; + const logger = AppObjectRegistry.get('logger'); + + const app = new appClass(appPackage.info, logger, AppAccessorsInstance.getDefaultAppAccessors()); + + if (typeof app.getName !== 'function') { + throw new Error('App must contain a getName function'); + } + + if (typeof app.getNameSlug !== 'function') { + throw new Error('App must contain a getNameSlug function'); + } + + if (typeof app.getVersion !== 'function') { + throw new Error('App must contain a getVersion function'); + } + + if (typeof app.getID !== 'function') { + throw new Error('App must contain a getID function'); + } + + if (typeof app.getDescription !== 'function') { + throw new Error('App must contain a getDescription function'); + } + + if (typeof app.getRequiredApiVersion !== 'function') { + throw new Error('App must contain a getRequiredApiVersion function'); + } + + AppObjectRegistry.set('app', app); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts new file mode 100644 index 000000000000..5428d989812e --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleGetStatus.ts @@ -0,0 +1,15 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; + +export default function handleGetStatus(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.getStatus !== 'function') { + throw new Error('App must contain a getStatus function', { + cause: 'invalid_app', + }); + } + + return app.getStatus(); +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts new file mode 100644 index 000000000000..ad90d3b01e25 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleInitialize.ts @@ -0,0 +1,19 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleInitialize(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.initialize !== 'function') { + throw new Error('App must contain an initialize function', { + cause: 'invalid_app', + }); + } + + await app.initialize(AppAccessorsInstance.getConfigurationExtend(), AppAccessorsInstance.getEnvironmentRead()); + + return true; +} + diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts new file mode 100644 index 000000000000..e66c2414fd0a --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnDisable.ts @@ -0,0 +1,19 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnDisable(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onDisable !== 'function') { + throw new Error('App must contain an onDisable function', { + cause: 'invalid_app', + }); + } + + await app.onDisable(AppAccessorsInstance.getConfigurationModify()); + + return true; +} + diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts new file mode 100644 index 000000000000..1bdf84476422 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnEnable.ts @@ -0,0 +1,16 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default function handleOnEnable(): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onEnable !== 'function') { + throw new Error('App must contain an onEnable function', { + cause: 'invalid_app', + }); + } + + return app.onEnable(AppAccessorsInstance.getEnvironmentRead(), AppAccessorsInstance.getConfigurationModify()); +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts new file mode 100644 index 000000000000..aebf7628a914 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnInstall.ts @@ -0,0 +1,30 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnInstall(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onInstall !== 'function') { + throw new Error('App must contain an onInstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onInstall( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts new file mode 100644 index 000000000000..19646fa6704f --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnPreSettingUpdate.ts @@ -0,0 +1,22 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default function handleOnPreSettingUpdate(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onPreSettingUpdate !== 'function') { + throw new Error('App must contain an onPreSettingUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + return app.onPreSettingUpdate(setting, AppAccessorsInstance.getConfigurationModify(), AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()); +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts new file mode 100644 index 000000000000..07084bc22425 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnSettingUpdated.ts @@ -0,0 +1,24 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnSettingUpdated(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onSettingUpdated !== 'function') { + throw new Error('App must contain an onSettingUpdated function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [setting] = params as [Record]; + + await app.onSettingUpdated(setting, AppAccessorsInstance.getConfigurationModify(), AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts new file mode 100644 index 000000000000..865819728ca4 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnUninstall.ts @@ -0,0 +1,30 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnUninstall(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUninstall !== 'function') { + throw new Error('App must contain an onUninstall function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUninstall( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts new file mode 100644 index 000000000000..f21e4f947d5d --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleOnUpdate.ts @@ -0,0 +1,30 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export default async function handleOnUpdate(params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + if (typeof app?.onUpdate !== 'function') { + throw new Error('App must contain an onUpdate function', { + cause: 'invalid_app', + }); + } + + if (!Array.isArray(params)) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [context] = params as [Record]; + + await app.onUpdate( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + + return true; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts new file mode 100644 index 000000000000..c39ab2a16d62 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handleSetStatus.ts @@ -0,0 +1,29 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { AppStatus as _AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { require } from '../../lib/require.ts'; + +const { AppStatus } = require('@rocket.chat/apps-engine/definition/AppStatus.js') as { + AppStatus: typeof _AppStatus; +}; + +export default async function handleSetStatus(params: unknown): Promise { + if (!Array.isArray(params) || !Object.values(AppStatus).includes(params[0])) { + throw new Error('Invalid params', { cause: 'invalid_param_type' }); + } + + const [status] = params as [typeof AppStatus]; + + const app = AppObjectRegistry.get('app'); + + if (!app || typeof app['setStatus'] !== 'function') { + throw new Error('App must contain a setStatus function', { + cause: 'invalid_app', + }); + } + + await app['setStatus'](status); + + return null; +} diff --git a/packages/apps-engine/deno-runtime/handlers/app/handler.ts b/packages/apps-engine/deno-runtime/handlers/app/handler.ts new file mode 100644 index 000000000000..2a44f34cb7fe --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/app/handler.ts @@ -0,0 +1,112 @@ +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import handleConstructApp from './construct.ts'; +import handleInitialize from './handleInitialize.ts'; +import handleGetStatus from './handleGetStatus.ts'; +import handleSetStatus from './handleSetStatus.ts'; +import handleOnEnable from './handleOnEnable.ts'; +import handleOnInstall from './handleOnInstall.ts'; +import handleOnDisable from './handleOnDisable.ts'; +import handleOnUninstall from './handleOnUninstall.ts'; +import handleOnPreSettingUpdate from './handleOnPreSettingUpdate.ts'; +import handleOnSettingUpdated from './handleOnSettingUpdated.ts'; +import handleListener from '../listener/handler.ts'; +import handleUIKitInteraction, { uikitInteractions } from '../uikit/handler.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import handleOnUpdate from './handleOnUpdate.ts'; + +export default async function handleApp(method: string, params: unknown): Promise { + const [, appMethod] = method.split(':'); + + // We don't want the getStatus method to generate logs, so we handle it separately + if (appMethod === 'getStatus') { + return handleGetStatus(); + } + + // `app` will be undefined if the method here is "app:construct" + const app = AppObjectRegistry.get('app'); + + app?.getLogger().debug(`'${appMethod}' is being called...`); + + if (uikitInteractions.includes(appMethod)) { + return handleUIKitInteraction(appMethod, params).then((result) => { + if (result instanceof JsonRpcError) { + app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); + } else { + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + } + + return result; + }); + } + + if (appMethod.startsWith('check') || appMethod.startsWith('execute')) { + return handleListener(appMethod, params).then((result) => { + if (result instanceof JsonRpcError) { + app?.getLogger().debug(`'${appMethod}' was unsuccessful.`, result.message); + } else { + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + } + + return result; + }); + } + + try { + let result: Defined | JsonRpcError; + + switch (appMethod) { + case 'construct': + result = await handleConstructApp(params); + break; + case 'initialize': + result = await handleInitialize(); + break; + case 'setStatus': + result = await handleSetStatus(params); + break; + case 'onEnable': + result = await handleOnEnable(); + break; + case 'onDisable': + result = await handleOnDisable(); + break; + case 'onInstall': + result = await handleOnInstall(params); + break; + case 'onUninstall': + result = await handleOnUninstall(params); + break; + case 'onPreSettingUpdate': + result = await handleOnPreSettingUpdate(params); + break; + case 'onSettingUpdated': + result = await handleOnSettingUpdated(params); + break; + case 'onUpdate': + result = await handleOnUpdate(params); + break; + default: + throw new JsonRpcError('Method not found', -32601); + } + + app?.getLogger().debug(`'${appMethod}' was successfully called! The result is:`, result); + + return result; + } catch (e: unknown) { + if (!(e instanceof Error)) { + return new JsonRpcError('Unknown error', -32000, e); + } + + if ((e.cause as string)?.includes('invalid_param_type')) { + return JsonRpcError.invalidParams(null); + } + + if ((e.cause as string)?.includes('invalid_app')) { + return JsonRpcError.internalError({ message: 'App unavailable' }); + } + + return new JsonRpcError(e.message, -32000, e); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/listener/handler.ts b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts new file mode 100644 index 000000000000..1e6de20538fc --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/listener/handler.ts @@ -0,0 +1,150 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { AppsEngineException as _AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; +import { AppAccessors, AppAccessorsInstance } from '../../lib/accessors/mod.ts'; +import { require } from '../../lib/require.ts'; +import createRoom from '../../lib/roomFactory.ts'; +import { Room } from "../../lib/room.ts"; + +const { AppsEngineException } = require('@rocket.chat/apps-engine/definition/exceptions/AppsEngineException.js') as { + AppsEngineException: typeof _AppsEngineException; +}; + +export default async function handleListener(evtInterface: string, params: unknown): Promise { + const app = AppObjectRegistry.get('app'); + + const eventExecutor = app?.[evtInterface as keyof App]; + + if (typeof eventExecutor !== 'function') { + return JsonRpcError.methodNotFound({ + message: 'Invalid event interface called on app', + }); + } + + if (!Array.isArray(params) || params.length < 1 || params.length > 2) { + return JsonRpcError.invalidParams(null); + } + + try { + const args = parseArgs({ AppAccessorsInstance }, evtInterface, params); + return await (eventExecutor as (...args: unknown[]) => Promise).apply(app, args); + } catch (e) { + if (e instanceof JsonRpcError) { + return e; + } + + if (e instanceof AppsEngineException) { + return new JsonRpcError(e.message, AppsEngineException.JSONRPC_ERROR_CODE, { name: e.name }); + } + + return JsonRpcError.internalError({ message: e.message }); + } + +} + +export function parseArgs(deps: { AppAccessorsInstance: AppAccessors }, evtMethod: string, params: unknown[]): unknown[] { + const { AppAccessorsInstance } = deps; + /** + * param1 is the context for the event handler execution + * param2 is an optional extra content that some hanlers require + */ + const [param1, param2] = params as [unknown, unknown]; + + if (!param1) { + throw JsonRpcError.invalidParams(null); + } + + let context = param1; + + if (evtMethod.includes('Message')) { + context = hydrateMessageObjects(context) as Record; + } else if (evtMethod.endsWith('RoomUserJoined') || evtMethod.endsWith('RoomUserLeave')) { + (context as Record).room = createRoom((context as Record).room as IRoom, AppAccessorsInstance.getSenderFn()); + } else if (evtMethod.includes('PreRoom')) { + context = createRoom(context as IRoom, AppAccessorsInstance.getSenderFn()); + } + + const args: unknown[] = [context, AppAccessorsInstance.getReader(), AppAccessorsInstance.getHttp()]; + + // "check" events will only go this far - (context, reader, http) + if (evtMethod.startsWith('check')) { + // "checkPostMessageDeleted" has an extra param - (context, reader, http, extraContext) + if (param2) { + args.push(hydrateMessageObjects(param2)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence) injected + args.push(AppAccessorsInstance.getPersistence()); + + // "extend" events have an additional "Extender" param - (context, extender, reader, http, persistence) + if (evtMethod.endsWith('Extend')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageExtender(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomExtender(param1 as IRoom)); + } + + return args; + } + + // "Modify" events have an additional "Builder" param - (context, builder, reader, http, persistence) + if (evtMethod.endsWith('Modify')) { + if (evtMethod.includes('Message')) { + args.splice(1, 0, new MessageBuilder(param1 as IMessage)); + } else if (evtMethod.includes('Room')) { + args.splice(1, 0, new RoomBuilder(param1 as IRoom)); + } + + return args; + } + + // From this point on, all events will require (reader, http, persistence, modifier) injected + args.push(AppAccessorsInstance.getModifier()); + + // This guy gets an extra one + if (evtMethod === 'executePostMessageDeleted') { + if (!param2) { + throw JsonRpcError.invalidParams(null); + } + + args.push(hydrateMessageObjects(param2)); + } + + return args; +} + +/** + * Hydrate the context object with the correct IMessage + * + * Some information is lost upon serializing the data from listeners through the pipes, + * so here we hydrate the complete object as necessary + */ +function hydrateMessageObjects(context: unknown): unknown { + if (objectIsRawMessage(context)) { + context.room = createRoom(context.room as IRoom, AppAccessorsInstance.getSenderFn()); + } else if ((context as Record)?.message) { + (context as Record).message = hydrateMessageObjects((context as Record).message); + } + + return context; +} + +function objectIsRawMessage(value: unknown): value is IMessage { + if (!value) return false; + + const { id, room, sender, createdAt } = value as Record; + + // Check if we have the fields of a message and the room hasn't already been hydrated + return !!(id && room && sender && createdAt) && !(room instanceof Room); +} diff --git a/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts new file mode 100644 index 000000000000..0145034957f2 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/scheduler-handler.ts @@ -0,0 +1,51 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; + +export default async function handleScheduler(method: string, params: unknown): Promise { + const [, processorId] = method.split(':'); + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams({ message: 'Invalid params' }); + } + + const [context] = params as [Record]; + + const app = AppObjectRegistry.get('app'); + + if (!app) { + return JsonRpcError.internalError({ message: 'App not found' }); + } + + // AppSchedulerManager will append the appId to the processor name to avoid conflicts + const processor = AppObjectRegistry.get(`scheduler:${processorId}`); + + if (!processor) { + return JsonRpcError.methodNotFound({ + message: `Could not find processor for method ${method}`, + }); + } + + app.getLogger().debug(`Job processor ${processor.id} is being executed...`); + + try { + await processor.processor( + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ); + + app.getLogger().debug(`Job processor ${processor.id} was successfully executed`); + + return null; + } catch (e) { + app.getLogger().error(e); + app.getLogger().error(`Job processor ${processor.id} was unsuccessful`); + + return JsonRpcError.internalError({ message: e.message }); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts new file mode 100644 index 000000000000..cfebf0d1460e --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/slashcommand-handler.ts @@ -0,0 +1,122 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; + +import type { App } from "@rocket.chat/apps-engine/definition/App.ts"; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { SlashCommandContext as _SlashCommandContext } from '@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.ts'; +import type { Room as _Room } from '@rocket.chat/apps-engine/server/rooms/Room.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessors, AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { require } from '../lib/require.ts'; +import createRoom from '../lib/roomFactory.ts'; + +// For some reason Deno couldn't understand the typecast to the original interfaces and said it wasn't a constructor type +const { SlashCommandContext } = require('@rocket.chat/apps-engine/definition/slashcommands/SlashCommandContext.js') as { + SlashCommandContext: typeof _SlashCommandContext; +}; + +export default async function slashCommandHandler(call: string, params: unknown): Promise { + const [, commandName, method] = call.split(':'); + + const command = AppObjectRegistry.get(`slashcommand:${commandName}`); + + if (!command) { + return new JsonRpcError(`Slashcommand ${commandName} not found`, -32000); + } + + let result: Awaited> | Awaited>; + + // If the command is registered, we're pretty safe to assume the app is not undefined + const app = AppObjectRegistry.get('app')!; + + app.getLogger().debug(`${commandName}'s ${method} is being executed...`, params); + + try { + if (method === 'executor' || method === 'previewer') { + result = await handleExecutor({ AppAccessorsInstance }, command, method, params); + } else if (method === 'executePreviewItem') { + result = await handlePreviewItem({ AppAccessorsInstance }, command, params); + } else { + return new JsonRpcError(`Method ${method} not found on slashcommand ${commandName}`, -32000); + } + + app.getLogger().debug(`${commandName}'s ${method} was successfully executed.`); + } catch (error) { + app.getLogger().debug(`${commandName}'s ${method} was unsuccessful.`); + + return new JsonRpcError(error.message, -32000); + } + + return result; +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param method The method that is being executed + * @param params The parameters that are being passed to the method + */ +export function handleExecutor(deps: { AppAccessorsInstance: AppAccessors }, command: ISlashCommand, method: 'executor' | 'previewer', params: unknown) { + const executor = command[method]; + + if (typeof executor !== 'function') { + throw new Error(`Method ${method} not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const { sender, room, params: args, threadId, triggerId } = params[0] as Record; + + const context = new SlashCommandContext( + sender as _SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), + args as _SlashCommandContext['params'], + threadId as _SlashCommandContext['threadId'], + triggerId as _SlashCommandContext['triggerId'], + ); + + return executor.apply(command, [ + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ]); +} + +/** + * @param deps Dependencies that need to be injected into the slashcommand + * @param command The slashcommand that is being executed + * @param params The parameters that are being passed to the method + */ +export function handlePreviewItem(deps: { AppAccessorsInstance: AppAccessors }, command: ISlashCommand, params: unknown) { + if (typeof command.executePreviewItem !== 'function') { + throw new Error(`Method not found on slashcommand ${command.command}`); + } + + if (!Array.isArray(params) || typeof params[0] !== 'object' || !params[0]) { + throw new Error(`First parameter must be an object`); + } + + const [previewItem, { sender, room, params: args, threadId, triggerId }] = params as [Record, Record]; + + const context = new SlashCommandContext( + sender as _SlashCommandContext['sender'], + createRoom(room as IRoom, deps.AppAccessorsInstance.getSenderFn()), + args as _SlashCommandContext['params'], + threadId as _SlashCommandContext['threadId'], + triggerId as _SlashCommandContext['triggerId'], + ); + + return command.executePreviewItem( + previewItem, + context, + deps.AppAccessorsInstance.getReader(), + deps.AppAccessorsInstance.getModifier(), + deps.AppAccessorsInstance.getHttp(), + deps.AppAccessorsInstance.getPersistence(), + ); +} diff --git a/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts new file mode 100644 index 000000000000..a3789f755542 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/api-handler.test.ts @@ -0,0 +1,79 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from "https://deno.land/std@0.203.0/testing/mock.ts"; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { assertInstanceOf } from "https://deno.land/std@0.203.0/assert/assert_instance_of.ts"; +import { JsonRpcError } from "jsonrpc-lite"; +import type { IApiEndpoint } from "@rocket.chat/apps-engine/definition/api/IApiEndpoint.ts"; +import apiHandler from "../api-handler.ts"; + +describe('handlers > api', () => { + const mockEndpoint: IApiEndpoint = { + path: '/test', + // deno-lint-ignore no-unused-vars + get: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + post: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => Promise.resolve('ok'), + // deno-lint-ignore no-unused-vars + put: (request: any, endpoint: any, read: any, modify: any, http: any, persis: any) => { throw new Error('Method execution error example') }, + } + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('api:/test', mockEndpoint); + }); + + it('correctly handles execution of an api endpoint method GET', async () => { + const _spy = spy(mockEndpoint, 'get'); + + const result = await apiHandler('api:/test:get', ['request', 'endpointInfo']); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles execution of an api endpoint method POST', async () => { + const _spy = spy(mockEndpoint, 'post'); + + const result = await apiHandler('api:/test:post', ['request', 'endpointInfo']); + + assertEquals(result, 'ok'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'request'); + assertEquals(_spy.calls[0].args[1], 'endpointInfo'); + }); + + it('correctly handles an error if the method not exists for the selected endpoint', async () => { + const result = await apiHandler(`api:/test:delete`, ['request', 'endpointInfo']); + + assertInstanceOf(result, JsonRpcError) + assertObjectMatch(result, { + message: `/test's delete not exists`, + code: -32000 + }) + }); + + it('correctly handles an error if endpoint not exists', async () => { + const result = await apiHandler(`api:/error:get`, ['request', 'endpointInfo']); + + assertInstanceOf(result, JsonRpcError) + assertObjectMatch(result, { + message: `Endpoint /error not found`, + code: -32000 + }) + }); + + it('correctly handles an error if the method execution fails', async () => { + const result = await apiHandler(`api:/test:put`, ['request', 'endpointInfo']); + + assertInstanceOf(result, JsonRpcError) + assertObjectMatch(result, { + message: `Method execution error example`, + code: -32000 + }) + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts new file mode 100644 index 000000000000..3e3663b06d22 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/listener-handler.test.ts @@ -0,0 +1,234 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { parseArgs } from '../listener/handler.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import { Room } from '../../lib/room.ts'; +import { MessageExtender } from '../../lib/accessors/extenders/MessageExtender.ts'; +import { RoomExtender } from '../../lib/accessors/extenders/RoomExtender.ts'; +import { MessageBuilder } from '../../lib/accessors/builders/MessageBuilder.ts'; +import { RoomBuilder } from '../../lib/accessors/builders/RoomBuilder.ts'; + +describe('handlers > listeners', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + it('correctly parses the arguments for a request to trigger the "checkPreMessageSentPrevent" method', () => { + const evtMethod = 'checkPreMessageSentPrevent'; + // For the 'checkPreMessageSentPrevent' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPostMessageDeleted" method', () => { + const evtMethod = 'checkPostMessageDeleted'; + // For the 'checkPostMessageDeleted' method, the context will be a message in a real scenario, + // and the extraContext will provide further information such the user who deleted the message + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 4); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "checkPreRoomCreateExtend" method', () => { + const evtMethod = 'checkPreRoomCreateExtend'; + // For the 'checkPreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [ + { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 3); + + assertInstanceOf(params[0], Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentExtend" method', () => { + const evtMethod = 'executePreMessageSentExtend'; + // For the 'executePreMessageSentExtend' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateExtend" method', () => { + const evtMethod = 'executePreRoomCreateExtend'; + // For the 'executePreRoomCreateExtend' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomExtender might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomExtender); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreMessageSentModify" method', () => { + const evtMethod = 'executePreMessageSentModify'; + // For the 'executePreMessageSentModify' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the MessageBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], MessageBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePreRoomCreateModify" method', () => { + const evtMethod = 'executePreRoomCreateModify'; + // For the 'executePreRoomCreateModify' method, the context will be a room in a real scenario + const evtArgs = [{ __type: 'context' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + // Instantiating the RoomBuilder might modify the original object, so we need to assert it matches instead of equals + assertObjectMatch(params[0] as Record, { + __type: 'context', + }); + assertInstanceOf(params[1], RoomBuilder); + assertEquals(params[2], { __type: 'reader' }); + assertEquals(params[3], { __type: 'http' }); + assertEquals(params[4], { __type: 'persistence' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserJoined" method', () => { + const evtMethod = 'executePostRoomUserJoined'; + // For the 'executePostRoomUserJoined' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostRoomUserLeave" method', () => { + const evtMethod = 'executePostRoomUserLeave'; + // For the 'executePostRoomUserLeave' method, the context will be a room in a real scenario + const room = { + id: 'fake', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }; + + const evtArgs = [{ __type: 'context', room }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageDeleted" method', () => { + const evtMethod = 'executePostMessageDeleted'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [{ __type: 'context' }, { __type: 'extraContext' }]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 6); + assertEquals(params[0], { __type: 'context' }); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + assertEquals(params[5], { __type: 'extraContext' }); + }); + + it('correctly parses the arguments for a request to trigger the "executePostMessageSent" method', () => { + const evtMethod = 'executePostMessageSent'; + // For the 'executePostMessageDeleted' method, the context will be a message in a real scenario + const evtArgs = [ + { + id: 'fake', + sender: 'fake', + createdAt: Date.now(), + room: { + id: 'fake-room', + type: 'fake', + slugifiedName: 'fake', + creator: 'fake', + createdAt: Date.now(), + }, + }, + ]; + + const params = parseArgs({ AppAccessorsInstance: mockAppAccessors }, evtMethod, evtArgs); + + assertEquals(params.length, 5); + assertObjectMatch((params[0] as Record), { id: 'fake' }); + assertInstanceOf((params[0] as any).room, Room); + assertEquals(params[1], { __type: 'reader' }); + assertEquals(params[2], { __type: 'http' }); + assertEquals(params[3], { __type: 'persistence' }); + assertEquals(params[4], { __type: 'modifier' }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts new file mode 100644 index 000000000000..681f228bdb3e --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/scheduler-handler.test.ts @@ -0,0 +1,46 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import handleScheduler from '../scheduler-handler.ts'; + +describe('handlers > scheduler', () => { + const mockAppAccessors = new AppAccessors(() => + Promise.resolve({ + id: 'mockId', + result: {}, + jsonrpc: '2.0', + serialize: () => '', + }), + ); + + const mockApp = { + getID: () => 'mockApp', + getLogger: () => ({ + debug: () => {}, + error: () => {}, + }), + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('app', mockApp); + mockAppAccessors.getConfigurationExtend().scheduler.registerProcessors([ + { + id: 'mockId', + processor: () => Promise.resolve('it works!'), + }, + ]); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly executes a request to a processor', async () => { + const result = await handleScheduler('scheduler:mockId', [{}]); + + assertEquals(result, null); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts new file mode 100644 index 000000000000..d3da4b132d66 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/slashcommand-handler.test.ts @@ -0,0 +1,152 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessors } from '../../lib/accessors/mod.ts'; +import { handleExecutor, handlePreviewItem } from '../slashcommand-handler.ts'; +import { Room } from "../../lib/room.ts"; + +describe('handlers > slashcommand', () => { + const mockAppAccessors = { + getReader: () => ({ __type: 'reader' }), + getHttp: () => ({ __type: 'http' }), + getModifier: () => ({ __type: 'modifier' }), + getPersistence: () => ({ __type: 'persistence' }), + getSenderFn: () => (id: string) => Promise.resolve([{ __type: 'bridgeCall' }, { id }]), + } as unknown as AppAccessors; + + const mockCommandExecutorOnly = { + command: 'executor-only', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: false, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandExecutorAndPreview = { + command: 'executor-and-preview', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async executor(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + const mockCommandPreviewWithNoExecutor = { + command: 'preview-with-no-executor', + i18nParamsExample: 'test', + i18nDescription: 'test', + providesPreview: true, + // deno-lint-ignore no-unused-vars + async previewer(context: any, read: any, modify: any, http: any, persis: any): Promise {}, + // deno-lint-ignore no-unused-vars + async executePreviewItem(previewItem: any, context: any, read: any, modify: any, http: any, persis: any): Promise {}, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('slashcommand:executor-only', mockCommandExecutorOnly); + AppObjectRegistry.set('slashcommand:executor-and-preview', mockCommandExecutorAndPreview); + AppObjectRegistry.set('slashcommand:preview-with-no-executor', mockCommandPreviewWithNoExecutor); + }); + + it('correctly handles execution of a slash command', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorOnly, 'executor'); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors }, mockCommandExecutorOnly, 'executor', [mockContext]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command previewer', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'previewer'); + + await handleExecutor({ AppAccessorsInstance: mockAppAccessors }, mockCommandExecutorAndPreview, 'previewer', [mockContext]); + + const context = _spy.calls[0].args[0]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[1], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); + + it('correctly handles execution of a slash command preview item executor', async () => { + const mockContext = { + sender: { __type: 'sender' }, + room: { __type: 'room' }, + params: { __type: 'params' }, + threadId: 'threadId', + triggerId: 'triggerId', + }; + + const mockPreviewItem = { + id: 'previewItemId', + type: 'image', + value: 'https://example.com/image.png', + }; + + const _spy = spy(mockCommandExecutorAndPreview, 'executePreviewItem'); + + await handlePreviewItem({ AppAccessorsInstance: mockAppAccessors }, mockCommandExecutorAndPreview, [mockPreviewItem, mockContext]); + + const context = _spy.calls[0].args[1]; + + assertInstanceOf(context.getRoom(), Room); + assertEquals(context.getSender(), { __type: 'sender' }); + assertEquals(context.getArguments(), { __type: 'params' }); + assertEquals(context.getThreadId(), 'threadId'); + assertEquals(context.getTriggerId(), 'triggerId'); + + assertEquals(_spy.calls[0].args[2], mockAppAccessors.getReader()); + assertEquals(_spy.calls[0].args[3], mockAppAccessors.getModifier()); + assertEquals(_spy.calls[0].args[4], mockAppAccessors.getHttp()); + assertEquals(_spy.calls[0].args[5], mockAppAccessors.getPersistence()); + + _spy.restore(); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts new file mode 100644 index 000000000000..f2293d6c98e0 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/uikit-handler.test.ts @@ -0,0 +1,99 @@ +// deno-lint-ignore-file no-explicit-any +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import handleUIKitInteraction, { + UIKitActionButtonInteractionContext, + UIKitBlockInteractionContext, + UIKitLivechatBlockInteractionContext, + UIKitViewCloseInteractionContext, + UIKitViewSubmitInteractionContext, +} from '../uikit/handler.ts'; + +describe('handlers > uikit', () => { + const mockApp = { + getID: (): string => 'appId', + executeBlockActionHandler: (context: any): Promise => Promise.resolve(context), + executeViewSubmitHandler: (context: any): Promise => Promise.resolve(context), + executeViewClosedHandler: (context: any): Promise => Promise.resolve(context), + executeActionButtonHandler: (context: any): Promise => Promise.resolve(context), + executeLivechatBlockActionHandler: (context: any): Promise => Promise.resolve(context), + }; + + beforeEach(() => { + AppObjectRegistry.set('app', mockApp); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('successfully handles a call for "executeBlockActionHandler"', async () => { + const result = await handleUIKitInteraction('executeBlockActionHandler', [ + { + actionId: 'actionId', + blockId: 'blockId', + value: 'value', + viewId: 'viewId', + }, + ]); + + assertInstanceOf(result, UIKitBlockInteractionContext); + }); + + it('successfully handles a call for "executeViewSubmitHandler"', async () => { + const result = await handleUIKitInteraction('executeViewSubmitHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + values: {}, + }, + ]); + + assertInstanceOf(result, UIKitViewSubmitInteractionContext); + }); + + it('successfully handles a call for "executeViewClosedHandler"', async () => { + const result = await handleUIKitInteraction('executeViewClosedHandler', [ + { + viewId: 'viewId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + assertInstanceOf(result, UIKitViewCloseInteractionContext); + }); + + it('successfully handles a call for "executeActionButtonHandler"', async () => { + const result = await handleUIKitInteraction('executeActionButtonHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + isAppUser: true, + }, + ]); + + assertInstanceOf(result, UIKitActionButtonInteractionContext); + }); + + it('successfully handles a call for "executeLivechatBlockActionHandler"', async () => { + const result = await handleUIKitInteraction('executeLivechatBlockActionHandler', [ + { + actionId: 'actionId', + appId: 'appId', + userId: 'userId', + visitor: {}, + isAppUser: true, + room: {}, + }, + ]); + + assertInstanceOf(result, UIKitLivechatBlockInteractionContext); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts new file mode 100644 index 000000000000..a32d3175e24d --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/tests/videoconference-handler.test.ts @@ -0,0 +1,122 @@ +// deno-lint-ignore-file no-explicit-any +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import videoconfHandler from '../videoconference-handler.ts'; +import { assertInstanceOf } from 'https://deno.land/std@0.203.0/assert/assert_instance_of.ts'; +import { JsonRpcError } from 'jsonrpc-lite'; + +describe('handlers > videoconference', () => { + // deno-lint-ignore no-unused-vars + const mockMethodWithoutParam = (read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok none'); + // deno-lint-ignore no-unused-vars + const mockMethodWithOneParam = (call: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok one'); + // deno-lint-ignore no-unused-vars + const mockMethodWithTwoParam = (call: any, user: any, read: any, modify: any, http: any, persis: any): Promise => Promise.resolve('ok two'); + // deno-lint-ignore no-unused-vars + const mockMethodWithThreeParam = (call: any, user: any, options: any, read: any, modify: any, http: any, persis: any): Promise => + Promise.resolve('ok three'); + const mockProvider = { + empty: mockMethodWithoutParam, + one: mockMethodWithOneParam, + two: mockMethodWithTwoParam, + three: mockMethodWithThreeParam, + notAFunction: true, + error: () => { + throw new Error('Method execution error example'); + }, + }; + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('videoConfProvider:test-provider', mockProvider); + }); + + it('correctly handles execution of a videoconf method without additional params', async () => { + const _spy = spy(mockProvider, 'empty'); + + const result = await videoconfHandler('videoconference:test-provider:empty', []); + + assertEquals(result, 'ok none'); + assertEquals(_spy.calls[0].args.length, 4); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with one param', async () => { + const _spy = spy(mockProvider, 'one'); + + const result = await videoconfHandler('videoconference:test-provider:one', ['call']); + + assertEquals(result, 'ok one'); + assertEquals(_spy.calls[0].args.length, 5); + assertEquals(_spy.calls[0].args[0], 'call'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with two params', async () => { + const _spy = spy(mockProvider, 'two'); + + const result = await videoconfHandler('videoconference:test-provider:two', ['call', 'user']); + + assertEquals(result, 'ok two'); + assertEquals(_spy.calls[0].args.length, 6); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + + _spy.restore(); + }); + + it('correctly handles execution of a videoconf method with three params', async () => { + const _spy = spy(mockProvider, 'three'); + + const result = await videoconfHandler('videoconference:test-provider:three', ['call', 'user', 'options']); + + assertEquals(result, 'ok three'); + assertEquals(_spy.calls[0].args.length, 7); + assertEquals(_spy.calls[0].args[0], 'call'); + assertEquals(_spy.calls[0].args[1], 'user'); + assertEquals(_spy.calls[0].args[2], 'options'); + + _spy.restore(); + }); + + it('correctly handles an error on execution of a videoconf method', async () => { + const result = await videoconfHandler('videoconference:test-provider:error', []); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method execution error example', + code: -32000, + }); + }); + + it('correctly handles an error when provider is not found', async () => { + const providerName = 'error-provider'; + const result = await videoconfHandler(`videoconference:${providerName}:method`, []); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: `Provider ${providerName} not found`, + code: -32000, + }); + }); + + it('correctly handles an error if method is not a function of provider', async () => { + const methodName = 'notAFunction'; + const providerName = 'test-provider'; + const result = await videoconfHandler(`videoconference:${providerName}:${methodName}`, []); + + assertInstanceOf(result, JsonRpcError); + assertObjectMatch(result, { + message: 'Method not found', + code: -32601, + data: { + message: `Method ${methodName} not found on provider ${providerName}`, + }, + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts new file mode 100644 index 000000000000..5a418f242ad9 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/uikit/handler.ts @@ -0,0 +1,82 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import { require } from '../../lib/require.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../../lib/accessors/mod.ts'; + +export const uikitInteractions = [ + 'executeBlockActionHandler', + 'executeViewSubmitHandler', + 'executeViewClosedHandler', + 'executeActionButtonHandler', + 'executeLivechatBlockActionHandler', +]; + +export const { + UIKitBlockInteractionContext, + UIKitViewSubmitInteractionContext, + UIKitViewCloseInteractionContext, + UIKitActionButtonInteractionContext, +} = require('@rocket.chat/apps-engine/definition/uikit/UIKitInteractionContext.js'); + +export const { UIKitLivechatBlockInteractionContext } = require('@rocket.chat/apps-engine/definition/uikit/livechat/UIKitLivechatInteractionContext.js'); + +export default async function handleUIKitInteraction(method: string, params: unknown): Promise { + if (!uikitInteractions.includes(method)) { + return JsonRpcError.methodNotFound(null); + } + + if (!Array.isArray(params)) { + return JsonRpcError.invalidParams(null); + } + + const app = AppObjectRegistry.get('app'); + + const interactionHandler = app?.[method as keyof App] as unknown; + + if (!app || typeof interactionHandler !== 'function') { + return JsonRpcError.methodNotFound({ + message: `App does not implement method "${method}"`, + }); + } + + const [payload] = params as [Record]; + + if (!payload) { + return JsonRpcError.invalidParams(null); + } + + let context; + + switch (method) { + case 'executeBlockActionHandler': + context = new UIKitBlockInteractionContext(payload); + break; + case 'executeViewSubmitHandler': + context = new UIKitViewSubmitInteractionContext(payload); + break; + case 'executeViewClosedHandler': + context = new UIKitViewCloseInteractionContext(payload); + break; + case 'executeActionButtonHandler': + context = new UIKitActionButtonInteractionContext(payload); + break; + case 'executeLivechatBlockActionHandler': + context = new UIKitLivechatBlockInteractionContext(payload); + break; + } + + try { + return await interactionHandler.call( + app, + context, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + AppAccessorsInstance.getModifier(), + ); + } catch (e) { + return JsonRpcError.internalError({ message: e.message }); + } +} diff --git a/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts new file mode 100644 index 000000000000..0347519db2e6 --- /dev/null +++ b/packages/apps-engine/deno-runtime/handlers/videoconference-handler.ts @@ -0,0 +1,49 @@ +import { Defined, JsonRpcError } from 'jsonrpc-lite'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import { AppAccessorsInstance } from '../lib/accessors/mod.ts'; +import { Logger } from '../lib/logger.ts'; + +export default async function videoConferenceHandler(call: string, params: unknown): Promise { + const [, providerName, methodName] = call.split(':'); + + const provider = AppObjectRegistry.get(`videoConfProvider:${providerName}`); + const logger = AppObjectRegistry.get('logger'); + + if (!provider) { + return new JsonRpcError(`Provider ${providerName} not found`, -32000); + } + + const method = provider[methodName as keyof IVideoConfProvider]; + + if (typeof method !== 'function') { + return JsonRpcError.methodNotFound({ + message: `Method ${methodName} not found on provider ${providerName}`, + }); + } + + const [videoconf, user, options] = params as Array; + + logger?.debug(`Executing ${methodName} on video conference provider...`); + + const args = [...(videoconf ? [videoconf] : []), ...(user ? [user] : []), ...(options ? [options] : [])]; + + try { + // deno-lint-ignore ban-types + const result = await (method as Function).apply(provider, [ + ...args, + AppAccessorsInstance.getReader(), + AppAccessorsInstance.getModifier(), + AppAccessorsInstance.getHttp(), + AppAccessorsInstance.getPersistence(), + ]); + + logger?.debug(`Video Conference Provider's ${methodName} was successfully executed.`); + + return result; + } catch (e) { + logger?.debug(`Video Conference Provider's ${methodName} was unsuccessful.`); + return new JsonRpcError(e.message, -32000); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts new file mode 100644 index 000000000000..e1602860fe97 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/BlockBuilder.ts @@ -0,0 +1,216 @@ +import { v1 as uuid } from 'uuid'; + +import type { + BlockType as _BlockType, + IActionsBlock, + IBlock, + IConditionalBlock, + IConditionalBlockFilters, + IContextBlock, + IImageBlock, + IInputBlock, + ISectionBlock, +} from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; +import type { + BlockElementType as _BlockElementType, + IBlockElement, + IButtonElement, + IImageElement, + IInputElement, + IInteractiveElement, + IMultiStaticSelectElement, + IOverflowMenuElement, + IPlainTextInputElement, + ISelectElement, + IStaticSelectElement, +} from '@rocket.chat/apps-engine/definition/uikit/blocks/Elements.ts'; +import type { ITextObject, TextObjectType as _TextObjectType } from '@rocket.chat/apps-engine/definition/uikit/blocks/Objects.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { require } from '../../../lib/require.ts'; + +const { BlockType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.js') as { BlockType: typeof _BlockType }; +const { BlockElementType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Elements.js') as { BlockElementType: typeof _BlockElementType }; +const { TextObjectType } = require('@rocket.chat/apps-engine/definition/uikit/blocks/Objects.js') as { TextObjectType: typeof _TextObjectType }; + +type BlockFunctionParameter = Omit; +type ElementFunctionParameter = T extends IInteractiveElement + ? Omit | Partial> + : Omit; + +type SectionBlockParam = BlockFunctionParameter; +type ImageBlockParam = BlockFunctionParameter; +type ActionsBlockParam = BlockFunctionParameter; +type ContextBlockParam = BlockFunctionParameter; +type InputBlockParam = BlockFunctionParameter; + +type ButtonElementParam = ElementFunctionParameter; +type ImageElementParam = ElementFunctionParameter; +type OverflowMenuElementParam = ElementFunctionParameter; +type PlainTextInputElementParam = ElementFunctionParameter; +type StaticSelectElementParam = ElementFunctionParameter; +type MultiStaticSelectElementParam = ElementFunctionParameter; + +/** + * @deprecated please prefer the rocket.chat/ui-kit components + */ +export class BlockBuilder { + private readonly blocks: Array; + private readonly appId: string; + + constructor() { + this.blocks = []; + this.appId = String(AppObjectRegistry.get('id')); + } + + public addSectionBlock(block: SectionBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.SECTION, ...block } as ISectionBlock); + + return this; + } + + public addImageBlock(block: ImageBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.IMAGE, ...block } as IImageBlock); + + return this; + } + + public addDividerBlock(): BlockBuilder { + this.addBlock({ type: BlockType.DIVIDER }); + + return this; + } + + public addActionsBlock(block: ActionsBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.ACTIONS, ...block } as IActionsBlock); + + return this; + } + + public addContextBlock(block: ContextBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.CONTEXT, ...block } as IContextBlock); + + return this; + } + + public addInputBlock(block: InputBlockParam): BlockBuilder { + this.addBlock({ type: BlockType.INPUT, ...block } as IInputBlock); + + return this; + } + + public addConditionalBlock(innerBlocks: BlockBuilder | Array, condition?: IConditionalBlockFilters): BlockBuilder { + const render = innerBlocks instanceof BlockBuilder ? innerBlocks.getBlocks() : innerBlocks; + + this.addBlock({ + type: BlockType.CONDITIONAL, + render, + when: condition, + } as IConditionalBlock); + + return this; + } + + public getBlocks() { + return this.blocks; + } + + public newPlainTextObject(text: string, emoji = false): ITextObject { + return { + type: TextObjectType.PLAINTEXT, + text, + emoji, + }; + } + + public newMarkdownTextObject(text: string): ITextObject { + return { + type: TextObjectType.MARKDOWN, + text, + }; + } + + public newButtonElement(info: ButtonElementParam): IButtonElement { + return this.newInteractiveElement({ + type: BlockElementType.BUTTON, + ...info, + } as IButtonElement); + } + + public newImageElement(info: ImageElementParam): IImageElement { + return { + type: BlockElementType.IMAGE, + ...info, + }; + } + + public newOverflowMenuElement(info: OverflowMenuElementParam): IOverflowMenuElement { + return this.newInteractiveElement({ + type: BlockElementType.OVERFLOW_MENU, + ...info, + } as IOverflowMenuElement); + } + + public newPlainTextInputElement(info: PlainTextInputElementParam): IPlainTextInputElement { + return this.newInputElement({ + type: BlockElementType.PLAIN_TEXT_INPUT, + ...info, + } as IPlainTextInputElement); + } + + public newStaticSelectElement(info: StaticSelectElementParam): IStaticSelectElement { + return this.newSelectElement({ + type: BlockElementType.STATIC_SELECT, + ...info, + } as IStaticSelectElement); + } + + public newMultiStaticElement(info: MultiStaticSelectElementParam): IMultiStaticSelectElement { + return this.newSelectElement({ + type: BlockElementType.MULTI_STATIC_SELECT, + ...info, + } as IMultiStaticSelectElement); + } + + private newInteractiveElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private newInputElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private newSelectElement(element: T): T { + if (!element.actionId) { + element.actionId = this.generateActionId(); + } + + return element; + } + + private addBlock(block: IBlock): void { + if (!block.blockId) { + block.blockId = this.generateBlockId(); + } + + block.appId = this.appId; + + this.blocks.push(block); + } + + private generateBlockId(): string { + return uuid(); + } + + private generateActionId(): string { + return uuid(); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts new file mode 100644 index 000000000000..e2c2dc021438 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/DiscussionBuilder.ts @@ -0,0 +1,59 @@ +import type { IDiscussionBuilder as _IDiscussionBuilder } from '@rocket.chat/apps-engine/definition/accessors/IDiscussionBuilder.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; + +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; + +import { RoomBuilder } from './RoomBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +export interface IDiscussionBuilder extends _IDiscussionBuilder, IRoomBuilder {} + +export class DiscussionBuilder extends RoomBuilder implements IDiscussionBuilder { + public kind: _RocketChatAssociationModel.DISCUSSION; + + private reply?: string; + + private parentMessage?: IMessage; + + constructor(data?: Partial) { + super(data); + this.kind = RocketChatAssociationModel.DISCUSSION; + this.room.type = RoomType.PRIVATE_GROUP; + } + + public setParentRoom(parentRoom: IRoom): IDiscussionBuilder { + this.room.parentRoom = parentRoom; + return this; + } + + public getParentRoom(): IRoom { + return this.room.parentRoom!; + } + + public setReply(reply: string): IDiscussionBuilder { + this.reply = reply; + return this; + } + + public getReply(): string { + return this.reply!; + } + + public setParentMessage(parentMessage: IMessage): IDiscussionBuilder { + this.parentMessage = parentMessage; + return this; + } + + public getParentMessage(): IMessage { + return this.parentMessage!; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts new file mode 100644 index 000000000000..a12024ab7b5d --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/LivechatMessageBuilder.ts @@ -0,0 +1,204 @@ +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; + +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { ILivechatMessage as EngineLivechatMessage } from '@rocket.chat/apps-engine/definition/livechat/ILivechatMessage.ts'; +import type { IVisitor } from '@rocket.chat/apps-engine/definition/livechat/IVisitor.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; + +import { MessageBuilder } from './MessageBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; + +export interface ILivechatMessage extends EngineLivechatMessage, IMessage {} + +export class LivechatMessageBuilder implements ILivechatMessageBuilder { + public kind: _RocketChatAssociationModel.LIVECHAT_MESSAGE; + + private msg: ILivechatMessage; + + constructor(message?: ILivechatMessage) { + this.kind = RocketChatAssociationModel.LIVECHAT_MESSAGE; + this.msg = message || ({} as ILivechatMessage); + } + + public setData(data: ILivechatMessage): ILivechatMessageBuilder { + delete data.id; + this.msg = data; + + return this; + } + + public setRoom(room: IRoom): ILivechatMessageBuilder { + this.msg.room = room; + return this; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): ILivechatMessageBuilder { + this.msg.sender = sender; + delete this.msg.visitor; + + return this; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): ILivechatMessageBuilder { + this.msg.text = text; + return this; + } + + public getText(): string { + return this.msg.text!; + } + + public setEmojiAvatar(emoji: string): ILivechatMessageBuilder { + this.msg.emoji = emoji; + return this; + } + + public getEmojiAvatar(): string { + return this.msg.emoji!; + } + + public setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder { + this.msg.avatarUrl = avatarUrl; + return this; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl!; + } + + public setUsernameAlias(alias: string): ILivechatMessageBuilder { + this.msg.alias = alias; + return this; + } + + public getUsernameAlias(): string { + return this.msg.alias!; + } + + public addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + return this; + } + + public setAttachments(attachments: Array): ILivechatMessageBuilder { + this.msg.attachments = attachments; + return this; + } + + public getAttachments(): Array { + return this.msg.attachments!; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + return this; + } + + public removeAttachment(position: number): ILivechatMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + + return this; + } + + public setEditor(user: IUser): ILivechatMessageBuilder { + this.msg.editor = user; + return this; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): ILivechatMessageBuilder { + this.msg.groupable = groupable; + return this; + } + + public getGroupable(): boolean { + return this.msg.groupable!; + } + + public setParseUrls(parseUrls: boolean): ILivechatMessageBuilder { + this.msg.parseUrls = parseUrls; + return this; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls!; + } + + public setToken(token: string): ILivechatMessageBuilder { + this.msg.token = token; + return this; + } + + public getToken(): string { + return this.msg.token!; + } + + public setVisitor(visitor: IVisitor): ILivechatMessageBuilder { + this.msg.visitor = visitor; + delete this.msg.sender; + + return this; + } + + public getVisitor(): IVisitor { + return this.msg.visitor; + } + + public getMessage(): ILivechatMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + if (this.msg.room.type !== RoomType.LIVE_CHAT) { + throw new Error('The room is not a Livechat room'); + } + + return this.msg; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(this.msg as IMessage); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts new file mode 100644 index 000000000000..98cd919f7b00 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -0,0 +1,232 @@ +import { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IBlock } from '@rocket.chat/apps-engine/definition/uikit/blocks/Blocks.ts'; + +import { BlockBuilder } from './BlockBuilder.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class MessageBuilder implements IMessageBuilder { + public kind: _RocketChatAssociationModel.MESSAGE; + + private msg: IMessage; + + constructor(message?: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + this.msg = message || ({} as IMessage); + } + + public setData(data: IMessage): IMessageBuilder { + delete data.id; + this.msg = data; + + return this as IMessageBuilder; + } + + public setUpdateData(data: IMessage, editor: IUser): IMessageBuilder { + this.msg = data; + this.msg.editor = editor; + this.msg.editedAt = new Date(); + + return this as IMessageBuilder; + } + + public setThreadId(threadId: string): IMessageBuilder { + this.msg.threadId = threadId; + + return this as IMessageBuilder; + } + + public getThreadId(): string { + return this.msg.threadId!; + } + + public setRoom(room: IRoom): IMessageBuilder { + this.msg.room = room; + return this as IMessageBuilder; + } + + public getRoom(): IRoom { + return this.msg.room; + } + + public setSender(sender: IUser): IMessageBuilder { + this.msg.sender = sender; + return this as IMessageBuilder; + } + + public getSender(): IUser { + return this.msg.sender; + } + + public setText(text: string): IMessageBuilder { + this.msg.text = text; + return this as IMessageBuilder; + } + + public getText(): string { + return this.msg.text!; + } + + public setEmojiAvatar(emoji: string): IMessageBuilder { + this.msg.emoji = emoji; + return this as IMessageBuilder; + } + + public getEmojiAvatar(): string { + return this.msg.emoji!; + } + + public setAvatarUrl(avatarUrl: string): IMessageBuilder { + this.msg.avatarUrl = avatarUrl; + return this as IMessageBuilder; + } + + public getAvatarUrl(): string { + return this.msg.avatarUrl!; + } + + public setUsernameAlias(alias: string): IMessageBuilder { + this.msg.alias = alias; + return this as IMessageBuilder; + } + + public getUsernameAlias(): string { + return this.msg.alias!; + } + + public addAttachment(attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + this.msg.attachments.push(attachment); + return this as IMessageBuilder; + } + + public setAttachments(attachments: Array): IMessageBuilder { + this.msg.attachments = attachments; + return this as IMessageBuilder; + } + + public getAttachments(): Array { + return this.msg.attachments!; + } + + public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to replace.`); + } + + this.msg.attachments[position] = attachment; + return this as IMessageBuilder; + } + + public removeAttachment(position: number): IMessageBuilder { + if (!this.msg.attachments) { + this.msg.attachments = []; + } + + if (!this.msg.attachments[position]) { + throw new Error(`No attachment found at the index of "${position}" to remove.`); + } + + this.msg.attachments.splice(position, 1); + + return this as IMessageBuilder; + } + + public setEditor(user: IUser): IMessageBuilder { + this.msg.editor = user; + return this as IMessageBuilder; + } + + public getEditor(): IUser { + return this.msg.editor; + } + + public setGroupable(groupable: boolean): IMessageBuilder { + this.msg.groupable = groupable; + return this as IMessageBuilder; + } + + public getGroupable(): boolean { + return this.msg.groupable!; + } + + public setParseUrls(parseUrls: boolean): IMessageBuilder { + this.msg.parseUrls = parseUrls; + return this as IMessageBuilder; + } + + public getParseUrls(): boolean { + return this.msg.parseUrls!; + } + + public getMessage(): IMessage { + if (!this.msg.room) { + throw new Error('The "room" property is required.'); + } + + return this.msg; + } + + public addBlocks(blocks: BlockBuilder | Array) { + if (!Array.isArray(this.msg.blocks)) { + this.msg.blocks = []; + } + + if (blocks instanceof BlockBuilder) { + this.msg.blocks.push(...blocks.getBlocks()); + } else { + this.msg.blocks.push(...blocks); + } + + return this as IMessageBuilder; + } + + public setBlocks(blocks: BlockBuilder | Array) { + if (blocks instanceof BlockBuilder) { + this.msg.blocks = blocks.getBlocks(); + } else { + this.msg.blocks = blocks; + } + + return this as IMessageBuilder; + } + + public getBlocks() { + return this.msg.blocks!; + } + + public addCustomField(key: string, value: unknown): IMessageBuilder { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + return this as IMessageBuilder; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts new file mode 100644 index 000000000000..38983475162d --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts @@ -0,0 +1,163 @@ +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; + +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class RoomBuilder implements IRoomBuilder { + public kind: _RocketChatAssociationModel.ROOM | _RocketChatAssociationModel.DISCUSSION; + + protected room: IRoom; + + private members: Array; + + constructor(data?: Partial) { + this.kind = RocketChatAssociationModel.ROOM; + this.room = (data || { customFields: {} }) as IRoom; + this.members = []; + } + + public setData(data: Partial): IRoomBuilder { + delete data.id; + this.room = data as IRoom; + + return this; + } + + public setDisplayName(name: string): IRoomBuilder { + this.room.displayName = name; + return this; + } + + public getDisplayName(): string { + return this.room.displayName!; + } + + public setSlugifiedName(name: string): IRoomBuilder { + this.room.slugifiedName = name; + return this; + } + + public getSlugifiedName(): string { + return this.room.slugifiedName; + } + + public setType(type: RoomType): IRoomBuilder { + this.room.type = type; + return this; + } + + public getType(): RoomType { + return this.room.type; + } + + public setCreator(creator: IUser): IRoomBuilder { + this.room.creator = creator; + return this; + } + + public getCreator(): IUser { + return this.room.creator; + } + + /** + * @deprecated + */ + public addUsername(username: string): IRoomBuilder { + this.addMemberToBeAddedByUsername(username); + return this; + } + + /** + * @deprecated + */ + public setUsernames(usernames: Array): IRoomBuilder { + this.setMembersToBeAddedByUsernames(usernames); + return this; + } + + /** + * @deprecated + */ + public getUsernames(): Array { + const usernames = this.getMembersToBeAddedUsernames(); + if (usernames && usernames.length > 0) { + return usernames; + } + return this.room.usernames || []; + } + + public addMemberToBeAddedByUsername(username: string): IRoomBuilder { + this.members.push(username); + return this; + } + + public setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder { + this.members = usernames; + return this; + } + + public getMembersToBeAddedUsernames(): Array { + return this.members; + } + + public setDefault(isDefault: boolean): IRoomBuilder { + this.room.isDefault = isDefault; + return this; + } + + public getIsDefault(): boolean { + return this.room.isDefault!; + } + + public setReadOnly(isReadOnly: boolean): IRoomBuilder { + this.room.isReadOnly = isReadOnly; + return this; + } + + public getIsReadOnly(): boolean { + return this.room.isReadOnly!; + } + + public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { + this.room.displaySystemMessages = displaySystemMessages; + return this; + } + + public getDisplayingOfSystemMessages(): boolean { + return this.room.displaySystemMessages!; + } + + public addCustomField(key: string, value: object): IRoomBuilder { + if (typeof this.room.customFields !== 'object') { + this.room.customFields = {}; + } + + this.room.customFields[key] = value; + return this; + } + + public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { + this.room.customFields = fields; + return this; + } + + public getCustomFields(): { [key: string]: object } { + return this.room.customFields!; + } + + public getUserIds(): Array { + return this.room.userIds!; + } + + public getRoom(): IRoom { + return this.room; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts new file mode 100644 index 000000000000..01c11a13f7a3 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/UserBuilder.ts @@ -0,0 +1,81 @@ +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IUserSettings } from '@rocket.chat/apps-engine/definition/users/IUserSettings.ts'; +import type { IUserEmail } from '@rocket.chat/apps-engine/definition/users/IUserEmail.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class UserBuilder implements IUserBuilder { + public kind: _RocketChatAssociationModel.USER; + + private user: Partial; + + constructor(user?: Partial) { + this.kind = RocketChatAssociationModel.USER; + this.user = user || ({} as Partial); + } + + public setData(data: Partial): IUserBuilder { + delete data.id; + this.user = data; + + return this; + } + + public setEmails(emails: Array): IUserBuilder { + this.user.emails = emails; + return this; + } + + public getEmails(): Array { + return this.user.emails!; + } + + public setDisplayName(name: string): IUserBuilder { + this.user.name = name; + return this; + } + + public getDisplayName(): string { + return this.user.name!; + } + + public setUsername(username: string): IUserBuilder { + this.user.username = username; + return this; + } + + public getUsername(): string { + return this.user.username!; + } + + public setRoles(roles: Array): IUserBuilder { + this.user.roles = roles; + return this; + } + + public getRoles(): Array { + return this.user.roles!; + } + + public getSettings(): Partial { + return this.user.settings; + } + + public getUser(): Partial { + if (!this.user.username) { + throw new Error('The "username" property is required.'); + } + + if (!this.user.name) { + throw new Error('The "name" property is required.'); + } + + return this.user; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts new file mode 100644 index 000000000000..e617cdddf154 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/VideoConferenceBuilder.ts @@ -0,0 +1,94 @@ +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; +import type { IGroupVideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; + +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export type AppVideoConference = Pick & { + createdBy: IGroupVideoConference['createdBy']['_id']; +}; + +export class VideoConferenceBuilder implements IVideoConferenceBuilder { + public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE = RocketChatAssociationModel.VIDEO_CONFERENCE; + + protected call: AppVideoConference; + + constructor(data?: Partial) { + this.call = (data || {}) as AppVideoConference; + } + + public setData(data: Partial): IVideoConferenceBuilder { + this.call = { + rid: data.rid!, + createdBy: data.createdBy, + providerName: data.providerName!, + title: data.title!, + discussionRid: data.discussionRid, + }; + + return this; + } + + public setRoomId(rid: string): IVideoConferenceBuilder { + this.call.rid = rid; + return this; + } + + public getRoomId(): string { + return this.call.rid; + } + + public setCreatedBy(userId: string): IVideoConferenceBuilder { + this.call.createdBy = userId; + return this; + } + + public getCreatedBy(): string { + return this.call.createdBy; + } + + public setProviderName(userId: string): IVideoConferenceBuilder { + this.call.providerName = userId; + return this; + } + + public getProviderName(): string { + return this.call.providerName; + } + + public setProviderData(data: Record | undefined): IVideoConferenceBuilder { + this.call.providerData = data; + return this; + } + + public getProviderData(): Record { + return this.call.providerData!; + } + + public setTitle(userId: string): IVideoConferenceBuilder { + this.call.title = userId; + return this; + } + + public getTitle(): string { + return this.call.title; + } + + public setDiscussionRid(rid: AppVideoConference['discussionRid']): IVideoConferenceBuilder { + this.call.discussionRid = rid; + return this; + } + + public getDiscussionRid(): AppVideoConference['discussionRid'] { + return this.call.discussionRid; + } + + public getVideoConference(): AppVideoConference { + return this.call; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts new file mode 100644 index 000000000000..c323d385de9d --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/HttpExtender.ts @@ -0,0 +1,62 @@ +import type { + IHttpExtend, + IHttpPreRequestHandler, + IHttpPreResponseHandler +} from "@rocket.chat/apps-engine/definition/accessors/IHttp.ts"; + +export class HttpExtend implements IHttpExtend { + private headers: Map; + + private params: Map; + + private requests: Array; + + private responses: Array; + + constructor() { + this.headers = new Map(); + this.params = new Map(); + this.requests = []; + this.responses = []; + } + + public provideDefaultHeader(key: string, value: string): void { + this.headers.set(key, value); + } + + public provideDefaultHeaders(headers: { [key: string]: string }): void { + Object.keys(headers).forEach((key) => this.headers.set(key, headers[key])); + } + + public provideDefaultParam(key: string, value: string): void { + this.params.set(key, value); + } + + public provideDefaultParams(params: { [key: string]: string }): void { + Object.keys(params).forEach((key) => this.params.set(key, params[key])); + } + + public providePreRequestHandler(handler: IHttpPreRequestHandler): void { + this.requests.push(handler); + } + + public providePreResponseHandler(handler: IHttpPreResponseHandler): void { + this.responses.push(handler); + } + + public getDefaultHeaders(): Map { + return new Map(this.headers); + } + + public getDefaultParams(): Map { + return new Map(this.params); + } + + public getPreRequestHandlers(): Array { + return Array.from(this.requests); + } + + public getPreResponseHandlers(): Array { + return Array.from(this.responses); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts new file mode 100644 index 000000000000..abf1629c760a --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/MessageExtender.ts @@ -0,0 +1,66 @@ +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageAttachment } from '@rocket.chat/apps-engine/definition/messages/IMessageAttachment.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class MessageExtender implements IMessageExtender { + public readonly kind: _RocketChatAssociationModel.MESSAGE; + + constructor(private msg: IMessage) { + this.kind = RocketChatAssociationModel.MESSAGE; + + if (!Array.isArray(msg.attachments)) { + this.msg.attachments = []; + } + } + + public addCustomField(key: string, value: unknown): IMessageExtender { + if (!this.msg.customFields) { + this.msg.customFields = {}; + } + + if (this.msg.customFields[key]) { + throw new Error(`The message already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.msg.customFields[key] = value; + + return this; + } + + public addAttachment(attachment: IMessageAttachment): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments!.push(attachment); + + return this; + } + + public addAttachments(attachments: Array): IMessageExtender { + this.ensureAttachment(); + + this.msg.attachments = this.msg.attachments!.concat(attachments); + + return this; + } + + public getMessage(): IMessage { + return structuredClone(this.msg); + } + + private ensureAttachment(): void { + if (!Array.isArray(this.msg.attachments)) { + this.msg.attachments = []; + } + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts new file mode 100644 index 000000000000..6509d5dae90e --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/RoomExtender.ts @@ -0,0 +1,61 @@ +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class RoomExtender implements IRoomExtender { + public kind: _RocketChatAssociationModel.ROOM; + + private members: Array; + + constructor(private room: IRoom) { + this.kind = RocketChatAssociationModel.ROOM; + this.members = []; + } + + public addCustomField(key: string, value: unknown): IRoomExtender { + if (!this.room.customFields) { + this.room.customFields = {}; + } + + if (this.room.customFields[key]) { + throw new Error(`The room already contains a custom field by the key: ${key}`); + } + + if (key.includes('.')) { + throw new Error(`The given key contains a period, which is not allowed. Key: ${key}`); + } + + this.room.customFields[key] = value; + + return this; + } + + public addMember(user: IUser): IRoomExtender { + if (this.members.find((u) => u.username === user.username)) { + throw new Error('The user is already in the room.'); + } + + this.members.push(user); + + return this; + } + + public getMembersBeingAdded(): Array { + return this.members; + } + + public getUsernamesOfMembersBeingAdded(): Array { + return this.members.map((u) => u.username); + } + + public getRoom(): IRoom { + return structuredClone(this.room); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts b/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts new file mode 100644 index 000000000000..9616bf619067 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/extenders/VideoConferenceExtend.ts @@ -0,0 +1,69 @@ +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts'; +import type { VideoConference, VideoConferenceMember } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; +import type { IVideoConferenceUser } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConferenceUser.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class VideoConferenceExtender implements IVideoConferenceExtender { + public kind: _RocketChatAssociationModel.VIDEO_CONFERENCE; + + constructor(private videoConference: VideoConference) { + this.kind = RocketChatAssociationModel.VIDEO_CONFERENCE; + } + + public setProviderData(value: Record): IVideoConferenceExtender { + this.videoConference.providerData = value; + + return this; + } + + public setStatus(value: VideoConference['status']): IVideoConferenceExtender { + this.videoConference.status = value; + + return this; + } + + public setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender { + this.videoConference.endedBy = { + _id: value, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }; + + return this; + } + + public setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender { + this.videoConference.endedAt = value; + + return this; + } + + public addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender { + this.videoConference.users.push({ + _id: userId, + ts, + // Name and username will be loaded automatically by the bridge + username: '', + name: '', + }); + + return this; + } + + public setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender { + this.videoConference.discussionRid = rid; + + return this; + } + + public getVideoConference(): VideoConference { + return structuredClone(this.videoConference); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/http.ts b/packages/apps-engine/deno-runtime/lib/accessors/http.ts new file mode 100644 index 000000000000..f55838e60186 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/http.ts @@ -0,0 +1,92 @@ +import type { + IHttp, + IHttpExtend, + IHttpRequest, + IHttpResponse, +} from "@rocket.chat/apps-engine/definition/accessors/IHttp.ts"; +import type { IPersistence } from "@rocket.chat/apps-engine/definition/accessors/IPersistence.ts"; +import type { IRead } from "@rocket.chat/apps-engine/definition/accessors/IRead.ts"; + +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from "../../AppObjectRegistry.ts"; + +type RequestMethod = 'get' | 'post' | 'put' | 'head' | 'delete' | 'patch'; + +export class Http implements IHttp { + private httpExtender: IHttpExtend; + private read: IRead; + private persistence: IPersistence; + private senderFn: typeof Messenger.sendRequest; + + constructor(read: IRead, persistence: IPersistence, httpExtender: IHttpExtend, senderFn: typeof Messenger.sendRequest) { + this.read = read; + this.persistence = persistence; + this.httpExtender = httpExtender; + this.senderFn = senderFn; + // this.httpExtender = new HttpExtend(); + } + + public get(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'get', options); + } + + public put(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'put', options); + } + + public post(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'post', options); + } + + public del(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'delete', options); + } + + public patch(url: string, options?: IHttpRequest): Promise { + return this._processHandler(url, 'patch', options); + } + + private async _processHandler(url: string, method: RequestMethod, options?: IHttpRequest): Promise { + let request = options || {}; + + if (typeof request.headers === 'undefined') { + request.headers = {}; + } + + this.httpExtender.getDefaultHeaders().forEach((value: string, key: string) => { + if (typeof request.headers?.[key] !== 'string') { + request.headers![key] = value; + } + }); + + if (typeof request.params === 'undefined') { + request.params = {}; + } + + this.httpExtender.getDefaultParams().forEach((value: string, key: string) => { + if (typeof request.params?.[key] !== 'string') { + request.params![key] = value; + } + }); + + for (const handler of this.httpExtender.getPreRequestHandlers()) { + request = await handler.executePreHttpRequest(url, request, this.read, this.persistence); + } + + let { result: response } = await this.senderFn({ + method: `bridges:getHttpBridge:doCall`, + params: [{ + appId: AppObjectRegistry.get('id'), + method, + url, + request, + }], + }) + + for (const handler of this.httpExtender.getPreResponseHandlers()) { + response = await handler.executePreHttpResponse(response as IHttpResponse, this.read, this.persistence); + } + + return response as IHttpResponse; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/mod.ts b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts new file mode 100644 index 000000000000..e71f014421ab --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/mod.ts @@ -0,0 +1,302 @@ +import type { IAppAccessors } from '@rocket.chat/apps-engine/definition/accessors/IAppAccessors.ts'; +import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api/IApiEndpointMetadata.ts'; +import type { IEnvironmentWrite } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentWrite.ts'; +import type { IEnvironmentRead } from '@rocket.chat/apps-engine/definition/accessors/IEnvironmentRead.ts'; +import type { IConfigurationModify } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationModify.ts'; +import type { IRead } from '@rocket.chat/apps-engine/definition/accessors/IRead.ts'; +import type { IModify } from '@rocket.chat/apps-engine/definition/accessors/IModify.ts'; +import type { INotifier } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { IPersistence } from '@rocket.chat/apps-engine/definition/accessors/IPersistence.ts'; +import type { IHttp, IHttpExtend } from '@rocket.chat/apps-engine/definition/accessors/IHttp.ts'; +import type { IConfigurationExtend } from '@rocket.chat/apps-engine/definition/accessors/IConfigurationExtend.ts'; +import type { ISlashCommand } from '@rocket.chat/apps-engine/definition/slashcommands/ISlashCommand.ts'; +import type { IProcessor } from '@rocket.chat/apps-engine/definition/scheduler/IProcessor.ts'; +import type { IApi } from '@rocket.chat/apps-engine/definition/api/IApi.ts'; +import type { IVideoConfProvider } from '@rocket.chat/apps-engine/definition/videoConfProviders/IVideoConfProvider.ts'; + +import { Http } from './http.ts'; +import { HttpExtend } from './extenders/HttpExtender.ts'; +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { ModifyCreator } from './modify/ModifyCreator.ts'; +import { ModifyUpdater } from './modify/ModifyUpdater.ts'; +import { ModifyExtender } from './modify/ModifyExtender.ts'; +import { Notifier } from './notifier.ts'; + +const httpMethods = ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'] as const; + +// We need to create this object first thing, as we'll handle references to it later on +if (!AppObjectRegistry.has('apiEndpoints')) { + AppObjectRegistry.set('apiEndpoints', []); +} + +export class AppAccessors { + private defaultAppAccessors?: IAppAccessors; + private environmentRead?: IEnvironmentRead; + private environmentWriter?: IEnvironmentWrite; + private configModifier?: IConfigurationModify; + private configExtender?: IConfigurationExtend; + private reader?: IRead; + private modifier?: IModify; + private persistence?: IPersistence; + private creator?: ModifyCreator; + private updater?: ModifyUpdater; + private extender?: ModifyExtender; + private httpExtend: IHttpExtend = new HttpExtend(); + private http?: IHttp; + private notifier?: INotifier; + + private proxify: (namespace: string, overrides?: Record unknown>) => T; + + constructor(private readonly senderFn: typeof Messenger.sendRequest) { + this.proxify = (namespace: string, overrides: Record unknown> = {}): T => + new Proxy( + { __kind: `accessor:${namespace}` }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => { + // We don't want to send a request for this prop + if (prop === 'toJSON') { + return {}; + } + + // If the prop is inteded to be overriden by the caller + if (prop in overrides) { + return overrides[prop].apply(undefined, params); + } + + return senderFn({ + method: `accessor:${namespace}:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { throw new Error(err.error) }); + }, + }, + ) as T; + + this.http = new Http(this.getReader(), this.getPersistence(), this.httpExtend, this.getSenderFn()); + this.notifier = new Notifier(this.getSenderFn()); + } + + public getSenderFn() { + return this.senderFn; + } + + public getEnvironmentRead(): IEnvironmentRead { + if (!this.environmentRead) { + this.environmentRead = { + getSettings: () => this.proxify('getEnvironmentRead:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentRead:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getEnvironmentRead:getEnvironmentVariables'), + }; + } + + return this.environmentRead; + } + + public getEnvironmentWrite() { + if (!this.environmentWriter) { + this.environmentWriter = { + getSettings: () => this.proxify('getEnvironmentWrite:getSettings'), + getServerSettings: () => this.proxify('getEnvironmentWrite:getServerSettings'), + }; + } + + return this.environmentWriter; + } + + public getConfigurationModify() { + if (!this.configModifier) { + this.configModifier = { + scheduler: this.proxify('getConfigurationModify:scheduler'), + slashCommands: { + _proxy: this.proxify('getConfigurationModify:slashCommands'), + modifySlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.modifySlashCommand(slashcommand); + }, + disableSlashCommand(command: string) { + return this._proxy.disableSlashCommand(command); + }, + enableSlashCommand(command: string) { + return this._proxy.enableSlashCommand(command); + }, + }, + serverSettings: this.proxify('getConfigurationModify:serverSettings'), + }; + } + + return this.configModifier; + } + + public getConfigurationExtend() { + if (!this.configExtender) { + const senderFn = this.senderFn; + + this.configExtender = { + ui: this.proxify('getConfigurationExtend:ui'), + http: this.httpExtend, + settings: this.proxify('getConfigurationExtend:settings'), + externalComponents: this.proxify('getConfigurationExtend:externalComponents'), + api: { + _proxy: this.proxify('getConfigurationExtend:api'), + async provideApi(api: IApi) { + const apiEndpoints = AppObjectRegistry.get('apiEndpoints')!; + + api.endpoints.forEach((endpoint) => { + endpoint._availableMethods = httpMethods.filter((method) => typeof endpoint[method] === 'function'); + + // We need to keep a reference to the endpoint around for us to call the executor later + AppObjectRegistry.set(`api:${endpoint.path}`, endpoint); + }); + + const result = await this._proxy.provideApi(api); + + // Let's call the listApis method to cache the info from the endpoints + // Also, since this is a side-effect, we do it async so we can return to the caller + senderFn({ method: 'accessor:api:listApis' }) + .then((response) => apiEndpoints.push(...(response.result as IApiEndpointMetadata[]))) + .catch((err) => err.error); + + return result; + }, + }, + scheduler: { + _proxy: this.proxify('getConfigurationExtend:scheduler'), + registerProcessors(processors: IProcessor[]) { + // Store the processor instance to use when the Apps-Engine calls the processor + processors.forEach((processor) => { + AppObjectRegistry.set(`scheduler:${processor.id}`, processor); + }); + + return this._proxy.registerProcessors(processors); + }, + }, + videoConfProviders: { + _proxy: this.proxify('getConfigurationExtend:videoConfProviders'), + provideVideoConfProvider(provider: IVideoConfProvider) { + // Store the videoConfProvider instance to use when the Apps-Engine calls the videoConfProvider + AppObjectRegistry.set(`videoConfProvider:${provider.name}`, provider); + + return this._proxy.provideVideoConfProvider(provider); + }, + }, + slashCommands: { + _proxy: this.proxify('getConfigurationExtend:slashCommands'), + provideSlashCommand(slashcommand: ISlashCommand) { + // Store the slashcommand instance to use when the Apps-Engine calls the slashcommand + AppObjectRegistry.set(`slashcommand:${slashcommand.command}`, slashcommand); + + return this._proxy.provideSlashCommand(slashcommand); + }, + }, + }; + } + + return this.configExtender; + } + + public getDefaultAppAccessors() { + if (!this.defaultAppAccessors) { + this.defaultAppAccessors = { + environmentReader: this.getEnvironmentRead(), + environmentWriter: this.getEnvironmentWrite(), + reader: this.getReader(), + http: this.getHttp(), + providedApiEndpoints: AppObjectRegistry.get('apiEndpoints') as IApiEndpointMetadata[], + }; + } + + return this.defaultAppAccessors; + } + + public getReader() { + if (!this.reader) { + this.reader = { + getEnvironmentReader: () => ({ + getSettings: () => this.proxify('getReader:getEnvironmentReader:getSettings'), + getServerSettings: () => this.proxify('getReader:getEnvironmentReader:getServerSettings'), + getEnvironmentVariables: () => this.proxify('getReader:getEnvironmentReader:getEnvironmentVariables'), + }), + getMessageReader: () => this.proxify('getReader:getMessageReader'), + getPersistenceReader: () => this.proxify('getReader:getPersistenceReader'), + getRoomReader: () => this.proxify('getReader:getRoomReader'), + getUserReader: () => this.proxify('getReader:getUserReader'), + getNotifier: () => this.getNotifier(), + getLivechatReader: () => this.proxify('getReader:getLivechatReader'), + getUploadReader: () => this.proxify('getReader:getUploadReader'), + getCloudWorkspaceReader: () => this.proxify('getReader:getCloudWorkspaceReader'), + getVideoConferenceReader: () => this.proxify('getReader:getVideoConferenceReader'), + getOAuthAppsReader: () => this.proxify('getReader:getOAuthAppsReader'), + getThreadReader: () => this.proxify('getReader:getThreadReader'), + getRoleReader: () => this.proxify('getReader:getRoleReader'), + }; + } + + return this.reader; + } + + public getModifier() { + if (!this.modifier) { + this.modifier = { + getCreator: this.getCreator.bind(this), + getUpdater: this.getUpdater.bind(this), + getExtender: this.getExtender.bind(this), + getDeleter: () => this.proxify('getModifier:getDeleter'), + getNotifier: () => this.getNotifier(), + getUiController: () => this.proxify('getModifier:getUiController'), + getScheduler: () => this.proxify('getModifier:getScheduler'), + getOAuthAppsModifier: () => this.proxify('getModifier:getOAuthAppsModifier'), + getModerationModifier: () => this.proxify('getModifier:getModerationModifier'), + }; + } + + return this.modifier; + } + + public getPersistence() { + if (!this.persistence) { + this.persistence = this.proxify('getPersistence'); + } + + return this.persistence; + } + + public getHttp() { + return this.http; + } + + private getCreator() { + if (!this.creator) { + this.creator = new ModifyCreator(this.senderFn); + } + + return this.creator; + } + + private getUpdater() { + if (!this.updater) { + this.updater = new ModifyUpdater(this.senderFn); + } + + return this.updater; + } + + private getExtender() { + if (!this.extender) { + this.extender = new ModifyExtender(this.senderFn); + } + + return this.extender; + } + + private getNotifier() { + return this.notifier; + } +} + +export const AppAccessorsInstance = new AppAccessors(Messenger.sendRequest); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts new file mode 100644 index 000000000000..06797551a621 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyCreator.ts @@ -0,0 +1,344 @@ +import type { IModifyCreator } from '@rocket.chat/apps-engine/definition/accessors/IModifyCreator.ts'; +import type { IUploadCreator } from '@rocket.chat/apps-engine/definition/accessors/IUploadCreator.ts'; +import type { IEmailCreator } from '@rocket.chat/apps-engine/definition/accessors/IEmailCreator.ts'; +import type { ILivechatCreator } from '@rocket.chat/apps-engine/definition/accessors/ILivechatCreator.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { IBotUser } from '@rocket.chat/apps-engine/definition/users/IBotUser.ts'; +import type { UserType as _UserType } from '@rocket.chat/apps-engine/definition/users/UserType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUserBuilder } from '@rocket.chat/apps-engine/definition/accessors/IUserBuilder.ts'; +import type { IVideoConferenceBuilder } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceBuilder.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { ILivechatMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/ILivechatMessageBuilder.ts'; +import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; + +import * as Messenger from '../../messenger.ts'; + +import { BlockBuilder } from '../builders/BlockBuilder.ts'; +import { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { DiscussionBuilder, IDiscussionBuilder } from '../builders/DiscussionBuilder.ts'; +import { ILivechatMessage, LivechatMessageBuilder } from '../builders/LivechatMessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { UserBuilder } from '../builders/UserBuilder.ts'; +import { AppVideoConference, VideoConferenceBuilder } from '../builders/VideoConferenceBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { require } from '../../../lib/require.ts'; + +const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { UserType } = require('@rocket.chat/apps-engine/definition/users/UserType.js') as { UserType: typeof _UserType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyCreator implements IModifyCreator { + constructor(private readonly senderFn: typeof Messenger.sendRequest) { } + + getLivechatCreator(): ILivechatCreator { + return new Proxy( + { __kind: 'getLivechatCreator' }, + { + get: (_target: unknown, prop: string) => { + // It's not worthwhile to make an asynchronous request for such a simple method + if (prop === 'createToken') { + return () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + } + + if (prop === 'toJSON') { + return () => ({}); + } + + return (...params: unknown[]) => + this.senderFn({ + method: `accessor:getModifier:getCreator:getLivechatCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }); + }, + }, + ) as ILivechatCreator; + } + + getUploadCreator(): IUploadCreator { + return new Proxy( + { __kind: 'getUploadCreator' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getUploadCreator:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + }, + ) as IUploadCreator; + } + + getEmailCreator(): IEmailCreator { + return new Proxy( + { __kind: 'getEmailCreator' }, + { + get: (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getCreator:getEmailCreator:${prop}`, + params + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + } + ) + } + + getBlockBuilder() { + return new BlockBuilder(); + } + + startMessage(data?: IMessage) { + if (data) { + delete data.id; + } + + return new MessageBuilder(data); + } + + startLivechatMessage(data?: ILivechatMessage) { + if (data) { + delete data.id; + } + + return new LivechatMessageBuilder(data); + } + + startRoom(data?: IRoom) { + if (data) { + // @ts-ignore - this has been imported from the Apps-Engine + delete data.id; + } + + return new RoomBuilder(data); + } + + startDiscussion(data?: Partial) { + if (data) { + delete data.id; + } + + return new DiscussionBuilder(data); + } + + startVideoConference(data?: Partial) { + return new VideoConferenceBuilder(data); + } + + startBotUser(data?: Partial) { + if (data) { + delete data.id; + + const { roles } = data; + + if (roles?.length) { + const hasRole = roles + .map((role: string) => role.toLocaleLowerCase()) + .some((role: string) => role === 'admin' || role === 'owner' || role === 'moderator'); + + if (hasRole) { + throw new Error('Invalid role assigned to the user. Should not be admin, owner or moderator.'); + } + } + + if (!data.type) { + data.type = UserType.BOT; + } + } + + return new UserBuilder(data); + } + + public finish( + builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder, + ): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as IMessageBuilder); + case RocketChatAssociationModel.LIVECHAT_MESSAGE: + return this._finishLivechatMessage(builder as ILivechatMessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as IRoomBuilder); + case RocketChatAssociationModel.DISCUSSION: + return this._finishDiscussion(builder as IDiscussionBuilder); + case RocketChatAssociationModel.VIDEO_CONFERENCE: + return this._finishVideoConference(builder as IVideoConferenceBuilder); + case RocketChatAssociationModel.USER: + return this._finishUser(builder as IUserBuilder); + default: + throw new Error('Invalid builder passed to the ModifyCreator.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + delete result.id; + + if (!result.sender || !result.sender.id) { + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doGetAppUser', + params: ['APP_ID'], + }); + + const appUser = response.result; + + if (!appUser) { + throw new Error('Invalid sender assigned to the message.'); + } + + result.sender = appUser; + } + + if (result.blocks?.length) { + // Can we move this elsewhere? This AppObjectRegistry usage doesn't really belong here, but where? + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doCreate', + params: [result, AppObjectRegistry.get('id')], + }); + + return String(response.result); + } + + private async _finishLivechatMessage(builder: ILivechatMessageBuilder): Promise { + if (builder.getSender() && !builder.getVisitor()) { + return this._finishMessage(builder.getMessageBuilder()); + } + + const result = builder.getMessage(); + delete result.id; + + if (!result.token && (!result.visitor || !result.visitor.token)) { + throw new Error('Invalid visitor sending the message'); + } + + result.token = result.visitor ? result.visitor.token : result.token; + + const response = await this.senderFn({ + method: 'bridges:getLivechatBridge:doCreateMessage', + params: [result, AppObjectRegistry.get('id')], + }); + + return String(response.result); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + delete result.id; + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator || !result.creator.id) { + throw new Error('Invalid creator assigned to the room.'); + } + } + + if (result.type !== RoomType.DIRECT_MESSAGE) { + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.slugifiedName || !result.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName || !result.displayName.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + } + + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doCreate', + params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }); + + return String(response.result); + } + + private async _finishDiscussion(builder: IDiscussionBuilder): Promise { + const room = builder.getRoom(); + delete room.id; + + if (!room.creator || !room.creator.id) { + throw new Error('Invalid creator assigned to the discussion.'); + } + + if (!room.slugifiedName || !room.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the discussion.'); + } + + if (!room.displayName || !room.displayName.trim()) { + throw new Error('Invalid displayName assigned to the discussion.'); + } + + if (!room.parentRoom || !room.parentRoom.id) { + throw new Error('Invalid parentRoom assigned to the discussion.'); + } + + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doCreateDiscussion', + params: [room, builder.getParentMessage(), builder.getReply(), builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }); + + return String(response.result); + } + + private async _finishVideoConference(builder: IVideoConferenceBuilder): Promise { + const videoConference = builder.getVideoConference(); + + if (!videoConference.createdBy) { + throw new Error('Invalid creator assigned to the video conference.'); + } + + if (!videoConference.providerName?.trim()) { + throw new Error('Invalid provider name assigned to the video conference.'); + } + + if (!videoConference.rid) { + throw new Error('Invalid roomId assigned to the video conference.'); + } + + const response = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doCreate', + params: [videoConference, AppObjectRegistry.get('id')], + }); + + return String(response.result); + } + + private async _finishUser(builder: IUserBuilder): Promise { + const user = builder.getUser(); + + const response = await this.senderFn({ + method: 'bridges:getUserBridge:doCreate', + params: [user, AppObjectRegistry.get('id')], + }); + + return String(response.result); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts new file mode 100644 index 000000000000..c0793d015c64 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyExtender.ts @@ -0,0 +1,93 @@ +import type { IModifyExtender } from '@rocket.chat/apps-engine/definition/accessors/IModifyExtender.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IMessageExtender } from '@rocket.chat/apps-engine/definition/accessors/IMessageExtender.ts'; +import type { IRoomExtender } from '@rocket.chat/apps-engine/definition/accessors/IRoomExtender.ts'; +import type { IVideoConferenceExtender } from '@rocket.chat/apps-engine/definition/accessors/IVideoConferenceExtend.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { VideoConference } from '@rocket.chat/apps-engine/definition/videoConferences/IVideoConference.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import * as Messenger from '../../messenger.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { MessageExtender } from '../extenders/MessageExtender.ts'; +import { RoomExtender } from '../extenders/RoomExtender.ts'; +import { VideoConferenceExtender } from '../extenders/VideoConferenceExtend.ts'; +import { require } from '../../../lib/require.ts'; + +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyExtender implements IModifyExtender { + constructor(private readonly senderFn: typeof Messenger.sendRequest) {} + + public async extendMessage(messageId: string, updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }); + + const msg = result.result as IMessage; + + msg.editor = updater; + msg.editedAt = new Date(); + + return new MessageExtender(msg); + } + + public async extendRoom(roomId: string, _updater: IUser): Promise { + const result = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }); + + const room = result.result as IRoom; + + room.updatedAt = new Date(); + + return new RoomExtender(room); + } + + public async extendVideoConference(id: string): Promise { + const result = await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doGetById', + params: [id, AppObjectRegistry.get('id')], + }); + + const call = result.result as VideoConference; + + call._updatedAt = new Date(); + + return new VideoConferenceExtender(call); + } + + public async finish(extender: IMessageExtender | IRoomExtender | IVideoConferenceExtender): Promise { + switch (extender.kind) { + case RocketChatAssociationModel.MESSAGE: + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [(extender as IMessageExtender).getMessage(), AppObjectRegistry.get('id')], + }); + break; + case RocketChatAssociationModel.ROOM: + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [ + (extender as IRoomExtender).getRoom(), + (extender as IRoomExtender).getUsernamesOfMembersBeingAdded(), + AppObjectRegistry.get('id'), + ], + }); + break; + case RocketChatAssociationModel.VIDEO_CONFERENCE: + await this.senderFn({ + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [(extender as IVideoConferenceExtender).getVideoConference(), AppObjectRegistry.get('id')], + }); + break; + default: + throw new Error('Invalid extender passed to the ModifyExtender.finish function.'); + } + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts new file mode 100644 index 000000000000..8befe7bfa983 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -0,0 +1,153 @@ +import type { IModifyUpdater } from '@rocket.chat/apps-engine/definition/accessors/IModifyUpdater.ts'; +import type { ILivechatUpdater } from '@rocket.chat/apps-engine/definition/accessors/ILivechatUpdater.ts'; +import type { IUserUpdater } from '@rocket.chat/apps-engine/definition/accessors/IUserUpdater.ts'; +import type { IMessageBuilder } from '@rocket.chat/apps-engine/definition/accessors/IMessageBuilder.ts'; +import type { IRoomBuilder } from '@rocket.chat/apps-engine/definition/accessors/IRoomBuilder.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages/IMessage.ts'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; + +import type { UIHelper as _UIHelper } from '@rocket.chat/apps-engine/server/misc/UIHelper.ts'; +import type { RoomType as _RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { RocketChatAssociationModel as _RocketChatAssociationModel } from '@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.ts'; + +import * as Messenger from '../../messenger.ts'; + +import { MessageBuilder } from '../builders/MessageBuilder.ts'; +import { RoomBuilder } from '../builders/RoomBuilder.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; + +import { require } from '../../../lib/require.ts'; + +const { UIHelper } = require('@rocket.chat/apps-engine/server/misc/UIHelper.js') as { UIHelper: typeof _UIHelper }; +const { RoomType } = require('@rocket.chat/apps-engine/definition/rooms/RoomType.js') as { RoomType: typeof _RoomType }; +const { RocketChatAssociationModel } = require('@rocket.chat/apps-engine/definition/metadata/RocketChatAssociations.js') as { + RocketChatAssociationModel: typeof _RocketChatAssociationModel; +}; + +export class ModifyUpdater implements IModifyUpdater { + constructor(private readonly senderFn: typeof Messenger.sendRequest) { } + + public getLivechatUpdater(): ILivechatUpdater { + return new Proxy( + { __kind: 'getLivechatUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:getLivechatUpdater:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + }, + ) as ILivechatUpdater; + } + + public getUserUpdater(): IUserUpdater { + return new Proxy( + { __kind: 'getUserUpdater' }, + { + get: + (_target: unknown, prop: string) => + (...params: unknown[]) => + prop === 'toJSON' + ? {} + : this.senderFn({ + method: `accessor:getModifier:getUpdater:getUserUpdater:${prop}`, + params, + }) + .then((response) => response.result) + .catch((err) => { + throw new Error(err.error); + }), + }, + ) as IUserUpdater; + } + + public async message(messageId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getMessageBridge:doGetById', + params: [messageId, AppObjectRegistry.get('id')], + }); + + return new MessageBuilder(response.result as IMessage); + } + + public async room(roomId: string, _updater: IUser): Promise { + const response = await this.senderFn({ + method: 'bridges:getRoomBridge:doGetById', + params: [roomId, AppObjectRegistry.get('id')], + }); + + return new RoomBuilder(response.result as IRoom); + } + + public finish(builder: IMessageBuilder | IRoomBuilder): Promise { + switch (builder.kind) { + case RocketChatAssociationModel.MESSAGE: + return this._finishMessage(builder as IMessageBuilder); + case RocketChatAssociationModel.ROOM: + return this._finishRoom(builder as IRoomBuilder); + default: + throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); + } + } + + private async _finishMessage(builder: IMessageBuilder): Promise { + const result = builder.getMessage(); + + if (!result.id) { + throw new Error("Invalid message, can't update a message without an id."); + } + + if (!result.sender?.id) { + throw new Error('Invalid sender assigned to the message.'); + } + + if (result.blocks?.length) { + result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); + } + + await this.senderFn({ + method: 'bridges:getMessageBridge:doUpdate', + params: [result, AppObjectRegistry.get('id')], + }); + } + + private async _finishRoom(builder: IRoomBuilder): Promise { + const result = builder.getRoom(); + + if (!result.id) { + throw new Error("Invalid room, can't update a room without an id."); + } + + if (!result.type) { + throw new Error('Invalid type assigned to the room.'); + } + + if (result.type !== RoomType.LIVE_CHAT) { + if (!result.creator || !result.creator.id) { + throw new Error('Invalid creator assigned to the room.'); + } + + if (!result.slugifiedName || !result.slugifiedName.trim()) { + throw new Error('Invalid slugifiedName assigned to the room.'); + } + } + + if (!result.displayName || !result.displayName.trim()) { + throw new Error('Invalid displayName assigned to the room.'); + } + + await this.senderFn({ + method: 'bridges:getRoomBridge:doUpdate', + params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + }); + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts b/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts new file mode 100644 index 000000000000..625d68c1039f --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/notifier.ts @@ -0,0 +1,75 @@ +import type { IMessageBuilder, INotifier } from '@rocket.chat/apps-engine/definition/accessors'; +import type { ITypingOptions } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { _TypingScope } from '@rocket.chat/apps-engine/definition/accessors/INotifier.ts'; +import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users'; +import { MessageBuilder } from './builders/MessageBuilder.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import * as Messenger from '../messenger.ts'; +import { require } from "../require.ts"; + +const { TypingScope } = require('@rocket.chat/apps-engine/definition/accessors/INotifier.js') as { + TypingScope: typeof _TypingScope; +}; + +export class Notifier implements INotifier { + private senderFn: typeof Messenger.sendRequest; + + constructor(senderFn: typeof Messenger.sendRequest) { + this.senderFn = senderFn; + } + + public async notifyUser(user: IUser, message: IMessage): Promise { + if (!message.sender || !message.sender.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyUser', [user, message, AppObjectRegistry.get('id')]); + } + + public async notifyRoom(room: IRoom, message: IMessage): Promise { + if (!message.sender || !message.sender.id) { + const appUser = await this.getAppUser(); + + message.sender = appUser; + } + + await this.callMessageBridge('doNotifyRoom', [room, message, AppObjectRegistry.get('id')]); + } + + public async typing(options: ITypingOptions): Promise<() => Promise> { + options.scope = options.scope || TypingScope.Room; + + if (!options.username) { + const appUser = await this.getAppUser(); + options.username = (appUser && appUser.name) || ''; + } + + const appId = AppObjectRegistry.get('id'); + + await this.callMessageBridge('doTyping', [{ ...options, isTyping: true }, appId]); + + return async () => { + await this.callMessageBridge('doTyping', [{ ...options, isTyping: false }, appId]); + }; + } + + public getMessageBuilder(): IMessageBuilder { + return new MessageBuilder(); + } + + private async callMessageBridge(method: string, params: Array): Promise { + await this.senderFn({ + method: `bridges:getMessageBridge:${method}`, + params, + }); + } + + private async getAppUser(): Promise { + const response = await this.senderFn({ method: 'bridges:getUserBridge:doGetAppUser', params: [AppObjectRegistry.get('id')] }); + return response.result; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts new file mode 100644 index 000000000000..04592eadd3db --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/AppAccessors.test.ts @@ -0,0 +1,122 @@ +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/assert_equals.ts'; + +import { AppAccessors } from '../mod.ts'; +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; + +describe('AppAccessors', () => { + let appAccessors: AppAccessors; + const senderFn = (r: object) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + appAccessors = new AppAccessors(senderFn); + AppObjectRegistry.clear(); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('creates the correct format for IRead calls', async () => { + const roomRead = appAccessors.getReader().getRoomReader(); + const room = await roomRead.getById('123'); + + assertEquals(room, { + params: ['123'], + method: 'accessor:getReader:getRoomReader:getById', + }); + }); + + it('creates the correct format for IEnvironmentRead calls from IRead', async () => { + const reader = appAccessors.getReader().getEnvironmentReader().getEnvironmentVariables(); + const room = await reader.getValueByName('NODE_ENV'); + + assertEquals(room, { + params: ['NODE_ENV'], + method: 'accessor:getReader:getEnvironmentReader:getEnvironmentVariables:getValueByName', + }); + }); + + it('creates the correct format for IEvironmentRead calls', async () => { + const envRead = appAccessors.getEnvironmentRead(); + const env = await envRead.getServerSettings().getValueById('123'); + + assertEquals(env, { + params: ['123'], + method: 'accessor:getEnvironmentRead:getServerSettings:getValueById', + }); + }); + + it('creates the correct format for IEvironmentWrite calls', async () => { + const envRead = appAccessors.getEnvironmentWrite(); + const env = await envRead.getServerSettings().incrementValue('123', 6); + + assertEquals(env, { + params: ['123', 6], + method: 'accessor:getEnvironmentWrite:getServerSettings:incrementValue', + }); + }); + + it('creates the correct format for IConfigurationModify calls', async () => { + const configModify = appAccessors.getConfigurationModify(); + const command = await configModify.slashCommands.modifySlashCommand({ + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }); + + assertEquals(command, { + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + method: 'accessor:getConfigurationModify:slashCommands:modifySlashCommand', + }); + }); + + it('correctly stores a reference to a slashcommand object and sends a request via proxy', async () => { + const configExtend = appAccessors.getConfigurationExtend(); + + const slashcommand = { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + executor() { + return Promise.resolve(); + }, + }; + + const result = await configExtend.slashCommands.provideSlashCommand(slashcommand); + + assertEquals(AppObjectRegistry.get('slashcommand:test'), slashcommand); + + // The function will not be serialized and sent to the main process + delete result.params[0].executor; + + assertEquals(result, { + method: 'accessor:getConfigurationExtend:slashCommands:provideSlashCommand', + params: [ + { + command: 'test', + i18nDescription: 'test', + i18nParamsExample: 'test', + providesPreview: true, + }, + ], + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts new file mode 100644 index 000000000000..5927869e6c84 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyCreator.test.ts @@ -0,0 +1,106 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assert, assertEquals, assertNotInstanceOf } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyCreator } from '../modify/ModifyCreator.ts'; + +describe('ModifyCreator', () => { + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: r, + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('sends the correct payload in the request to create a message', async () => { + const spying = spy(senderFn); + const modifyCreator = new ModifyCreator(spying); + const messageBuilder = modifyCreator.startMessage(); + + // Importing types from the Apps-Engine is problematic, so we'll go with `any` here + messageBuilder + .setRoom({ id: '123' } as any) + .setSender({ id: '456' } as any) + .setText('Hello World') + .setUsernameAlias('alias') + .setAvatarUrl('https://avatars.com/123'); + + // We can't get a legitimate return value here, so we ignore it + // but we need to know that the request sent was well formed + await modifyCreator.finish(messageBuilder); + + assertSpyCall(spying, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doCreate', + params: [ + { + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + alias: 'alias', + avatarUrl: 'https://avatars.com/123', + }, + 'deno-test', + ], + }, + ], + }); + }); + + it('sends the correct payload in the request to upload a buffer', async () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = await modifyCreator.getUploadCreator().uploadBuffer(new Uint8Array([1, 2, 3, 4]), 'text/plain'); + + assertEquals(result, { + method: 'accessor:getModifier:getCreator:getUploadCreator:uploadBuffer', + params: [new Uint8Array([1, 2, 3, 4]), 'text/plain'], + }); + }); + + it('sends the correct payload in the request to create a visitor', async () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = (await modifyCreator.getLivechatCreator().createVisitor({ + token: 'random token', + username: 'random username for visitor', + name: 'Random Visitor', + })) as any; // We modified the send function so it changed the original return type of the function + + assertEquals(result, { + method: 'accessor:getModifier:getCreator:getLivechatCreator:createVisitor', + params: [ + { + token: 'random token', + username: 'random username for visitor', + name: 'Random Visitor', + }, + ], + }); + }); + + // This test is important because if we return a promise we break API compatibility + it('does not return a promise for calls of the createToken() method of the LivechatCreator', () => { + const modifyCreator = new ModifyCreator(senderFn); + + const result = modifyCreator.getLivechatCreator().createToken(); + + assertNotInstanceOf(result, Promise); + assert(typeof result === 'string', `Expected "${result}" to be of type "string", but got "${typeof result}"`); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts new file mode 100644 index 000000000000..1ec056e02ce3 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyExtender.test.ts @@ -0,0 +1,120 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyExtender } from '../modify/ModifyExtender.ts'; + +describe('ModifyExtender', () => { + let extender: ModifyExtender; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + extender = new ModifyExtender(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the extend message requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const messageExtender = await extender.extendMessage('message-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['message-id', 'deno-test'], + }, + ], + }); + + messageExtender.addCustomField('key', 'value'); + + await extender.finish(messageExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageExtender.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend room requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const roomExtender = await extender.extendRoom('room-id', { _id: 'user-id' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['room-id', 'deno-test'], + }, + ], + }); + + roomExtender.addCustomField('key', 'value'); + + await extender.finish(roomExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomExtender.getRoom(), [], 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the extend video conference requests', async () => { + const _spy = spy(extender, 'senderFn' as keyof ModifyExtender); + + const videoConferenceExtender = await extender.extendVideoConference('video-conference-id'); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doGetById', + params: ['video-conference-id', 'deno-test'], + }, + ], + }); + + videoConferenceExtender.setStatus(4); + + await extender.finish(videoConferenceExtender); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getVideoConferenceBridge:doUpdate', + params: [videoConferenceExtender.getVideoConference(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts new file mode 100644 index 000000000000..313275c967cf --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -0,0 +1,128 @@ +// deno-lint-ignore-file no-explicit-any +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { assertSpyCall, spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; + +import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; +import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; + +describe('ModifyUpdater', () => { + let modifyUpdater: ModifyUpdater; + + const senderFn = (r: any) => + Promise.resolve({ + id: Math.random().toString(36).substring(2), + jsonrpc: '2.0', + result: structuredClone(r), + serialize() { + return JSON.stringify(this); + }, + }); + + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('id', 'deno-test'); + modifyUpdater = new ModifyUpdater(senderFn); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + }); + + it('correctly formats requests for the update message flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const messageBuilder = await modifyUpdater.message('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getMessageBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + messageBuilder.setUpdateData( + { + id: '123', + room: { id: '123' }, + sender: { id: '456' }, + text: 'Hello World', + }, + { + id: '456', + }, + ); + + await modifyUpdater.finish(messageBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getMessageBridge:doUpdate', + params: [messageBuilder.getMessage(), 'deno-test'], + }, + ], + }); + + _spy.restore(); + }); + + it('correctly formats requests for the update room flow', async () => { + const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); + + const roomBuilder = await modifyUpdater.room('123', { id: '456' } as any); + + assertSpyCall(_spy, 0, { + args: [ + { + method: 'bridges:getRoomBridge:doGetById', + params: ['123', 'deno-test'], + }, + ], + }); + + roomBuilder.setData({ + id: '123', + type: 'c', + displayName: 'Test Room', + slugifiedName: 'test-room', + creator: { id: '456' }, + }); + + roomBuilder.setMembersToBeAddedByUsernames(['username1', 'username2']); + + // We need to sneak in the id as the `modifyUpdater.room` call won't have legitimate data + roomBuilder.getRoom().id = '123'; + + await modifyUpdater.finish(roomBuilder); + + assertSpyCall(_spy, 1, { + args: [ + { + method: 'bridges:getRoomBridge:doUpdate', + params: [roomBuilder.getRoom(), roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + }, + ], + }); + }); + + it('correctly formats requests to UserUpdater methods', async () => { + const result = await modifyUpdater.getUserUpdater().updateStatusText({ id: '123' } as any, 'Hello World') as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getUserUpdater:updateStatusText', + params: [{ id: '123' }, 'Hello World'], + }); + }); + + it('correctly formats requests to LivechatUpdater methods', async () => { + const result = await modifyUpdater.getLivechatUpdater().closeRoom({ id: '123' } as any, 'close it!') as any; + + assertEquals(result, { + method: 'accessor:getModifier:getUpdater:getLivechatUpdater:closeRoom', + params: [{ id: '123' }, 'close it!'], + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/ast/mod.ts b/packages/apps-engine/deno-runtime/lib/ast/mod.ts new file mode 100644 index 000000000000..09f4994f2bad --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/mod.ts @@ -0,0 +1,64 @@ +import { generate } from "astring"; +// @deno-types="../../acorn.d.ts" +import { Program, parse } from "acorn"; +// @deno-types="../../acorn-walk.d.ts" +import { fullAncestor } from "acorn-walk"; + +import * as operations from "./operations.ts"; +import type { WalkerState } from "./operations.ts"; + +function fixAst(ast: Program): boolean { + const pendingOperations = [ + operations.fixLivechatIsOnlineCalls, + operations.checkReassignmentOfModifiedIdentifiers, + operations.fixRoomUsernamesCalls, + ]; + + // Have we touched the tree? + let isModified = false; + + while (pendingOperations.length) { + const ops = pendingOperations.splice(0); + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + }; + + fullAncestor(ast, (node, state, ancestors, type) => { + ops.forEach(operation => operation(node, state, ancestors, type)); + }, undefined, state); + + if (state.isModified) { + isModified = true; + } + + if (state.functionIdentifiers.size) { + pendingOperations.push( + operations.buildFixModifiedFunctionsOperation(state.functionIdentifiers), + operations.checkReassignmentOfModifiedIdentifiers + ); + } + } + + return isModified; +} + +export function fixBrokenSynchronousAPICalls(appSource: string): string { + const astRootNode = parse(appSource, { + ecmaVersion: 2017, + // Allow everything, we don't want to complain if code is badly written + // Also, since the code itself has been transpiled, the chance of getting + // shenanigans is lower + allowReserved: true, + allowReturnOutsideFunction: true, + allowImportExportEverywhere: true, + allowAwaitOutsideFunction: true, + allowSuperOutsideMethod: true, + }); + + if (fixAst(astRootNode)) { + return generate(astRootNode); + } + + return appSource; +} diff --git a/packages/apps-engine/deno-runtime/lib/ast/operations.ts b/packages/apps-engine/deno-runtime/lib/ast/operations.ts new file mode 100644 index 000000000000..d3886348041f --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/operations.ts @@ -0,0 +1,239 @@ +// @deno-types="../../acorn.d.ts" +import { AnyNode, AssignmentExpression, AwaitExpression, Expression, Function, Identifier, MethodDefinition, Property } from 'acorn'; +// @deno-types="../../acorn-walk.d.ts" +import { FullAncestorWalkerCallback } from 'acorn-walk'; + +export type WalkerState = { + isModified: boolean; + functionIdentifiers: Set; +}; + +export function getFunctionIdentifier(ancestors: AnyNode[], functionNodeIndex: number) { + const parent = ancestors[functionNodeIndex - 1]; + + // If there is a parent node and it's not a computed property, we can try to + // extract an identifier for our function from it. This needs to be done first + // because when functions are assigned to named symbols, this will be the only + // way to call it, even if the function itself has an identifier + // Consider the following block: + // + // const foo = function bar() {} + // + // Even though the function itself has a name, the only way to call it in the + // program is wiht `foo()` + if (parent && !(parent as Property | MethodDefinition).computed) { + // Several node types can have an id prop of type Identifier + const { id } = parent as unknown as { id?: Identifier }; + if (id?.type === 'Identifier') { + return id.name; + } + + // Usually assignments to object properties (MethodDefinition, Property) + const { key } = parent as MethodDefinition | Property; + if (key?.type === 'Identifier') { + return key.name; + } + + // Variable assignments have left hand side that can be used as Identifier + const { left } = parent as AssignmentExpression; + + // Simple assignment: `const fn = () => {}` + if (left?.type === 'Identifier') { + return left.name; + } + + // Object property assignment: `obj.fn = () => {}` + if (left?.type === 'MemberExpression' && !left.computed) { + return (left.property as Identifier).name; + } + } + + // nodeIndex needs to be the index of a Function node (either FunctionDeclaration or FunctionExpression) + const currentNode = ancestors[functionNodeIndex] as Function; + + // Function declarations or expressions can be directly named + if (currentNode.id?.type === 'Identifier') { + return currentNode.id.name; + } +} + +export function wrapWithAwait(node: Expression) { + if (!node.type.endsWith('Expression')) { + throw new Error(`Can't wrap "${node.type}" with await`); + } + + const innerNode: Expression = { ...node }; + + node.type = 'AwaitExpression'; + // starting here node has become an AwaitExpression + (node as AwaitExpression).argument = innerNode; + + Object.keys(node).forEach((key) => !['type', 'argument'].includes(key) && delete node[key as keyof AnyNode]); +} + +export function asyncifyScope(ancestors: AnyNode[], state: WalkerState) { + const functionNodeIndex = ancestors.findLastIndex((n) => 'async' in n); + if (functionNodeIndex === -1) return; + + // At this point this is a node with an "async" property, so it has to be + // of type Function - let TS know about that + const functionScopeNode = ancestors[functionNodeIndex] as Function; + + if (functionScopeNode.async) { + return; + } + + functionScopeNode.async = true; + + // If the parent of a function node is a call expression, we're talking about an IIFE + // Should we care about this case as well? + // const parentNode = ancestors[functionScopeIndex-1]; + // if (parentNode?.type === 'CallExpression' && ancestors[functionScopeIndex-2] && ancestors[functionScopeIndex-2].type !== 'AwaitExpression') { + // pendingOperations.push(buildFunctionPredicate(getFunctionIdentifier(ancestors, functionScopeIndex-2))); + // } + + const identifier = getFunctionIdentifier(ancestors, functionNodeIndex); + + // We can't fix calls of functions which name we can't determine at compile time + if (!identifier) return; + + state.functionIdentifiers.add(identifier); +} + +export function buildFixModifiedFunctionsOperation(functionIdentifiers: Set): FullAncestorWalkerCallback { + return function _fixModifiedFunctionsOperation(node, state, ancestors) { + if (node.type !== 'CallExpression') return; + + let isWrappable = false; + + // This node is a simple call to a function, like `fn()` + isWrappable = node.callee.type === 'Identifier' && functionIdentifiers.has(node.callee.name); + + // This node is a call to an object property or instance method, like `obj.fn()`, but not computed like `obj[fn]()` + isWrappable ||= + node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.property?.type === 'Identifier' && + functionIdentifiers.has(node.callee.property.name); + + // This is a weird dereferencing technique used by bundlers, and since we'll be dealing with bundled sources we have to check for it + // e.g. `r=(0,fn)(e)` + if (!isWrappable && node.callee.type === 'SequenceExpression') { + const [, secondExpression] = node.callee.expressions; + isWrappable = secondExpression?.type === 'Identifier' && functionIdentifiers.has(secondExpression.name); + isWrappable ||= + secondExpression?.type === 'MemberExpression' && + !secondExpression.computed && + secondExpression.property.type === 'Identifier' && + functionIdentifiers.has(secondExpression.property.name); + } + + if (!isWrappable) return; + + // ancestors[ancestors.length-1] === node, so here we're checking for parent node + const parentNode = ancestors[ancestors.length - 2]; + if (!parentNode || parentNode.type === 'AwaitExpression') return; + + wrapWithAwait(node); + asyncifyScope(ancestors, state); + + state.isModified = true; + }; +} + +export const checkReassignmentOfModifiedIdentifiers: FullAncestorWalkerCallback = (node, { functionIdentifiers }, _ancestors) => { + if (node.type === 'AssignmentExpression') { + if (node.operator !== '=') return; + + let identifier = ''; + + if (node.left.type === 'Identifier') identifier = node.left.name; + + if (node.left.type === 'MemberExpression' && !node.left.computed) { + identifier = (node.left.property as Identifier).name; + } + + if (!identifier || node.right.type !== 'Identifier' || !functionIdentifiers.has(node.right.name)) return; + + functionIdentifiers.add(identifier); + + return; + } + + if (node.type === 'VariableDeclarator') { + if (node.id.type !== 'Identifier' || functionIdentifiers.has(node.id.name)) return; + + if (node.init?.type !== 'Identifier' || !functionIdentifiers.has(node.init?.name)) return; + + functionIdentifiers.add(node.id.name); + + return; + } + + // "Property" is for plain objects, "PropertyDefinition" is for classes + // but both share the same structure + if (node.type === 'Property' || node.type === 'PropertyDefinition') { + if (node.key.type !== 'Identifier' || functionIdentifiers.has(node.key.name)) return; + + if (node.value?.type !== 'Identifier' || !functionIdentifiers.has(node.value.name)) return; + + functionIdentifiers.add(node.key.name); + + return; + } +}; + +export const fixLivechatIsOnlineCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'isOnline') return; + + if (node.object.type !== 'CallExpression') return; + + if (node.object.callee.type !== 'MemberExpression') return; + + if ((node.object.callee.property as Identifier).name !== 'getLivechatReader') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + // If we're in the middle of a chained member access, we can't wrap with await + if (ancestors[parentIndex].type === 'MemberExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +}; + +export const fixRoomUsernamesCalls: FullAncestorWalkerCallback = (node, state, ancestors) => { + if (node.type !== 'MemberExpression' || node.computed) return; + + if ((node.property as Identifier).name !== 'usernames') return; + + let parentIndex = ancestors.length - 2; + let targetNode = ancestors[parentIndex]; + + if (targetNode.type !== 'CallExpression') { + targetNode = node; + } else { + parentIndex--; + } + + // If we're already wrapped with an await, nothing to do + if (ancestors[parentIndex].type === 'AwaitExpression') return; + + wrapWithAwait(targetNode); + asyncifyScope(ancestors, state); + + state.isModified = true; +} diff --git a/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts b/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts new file mode 100644 index 000000000000..330d2bf52620 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/tests/data/ast_blocks.ts @@ -0,0 +1,436 @@ +// @deno-types="../../../../acorn.d.ts" +import { AnyNode, ClassDeclaration, ExpressionStatement, FunctionDeclaration, VariableDeclaration } from 'acorn'; + +/** + * Partial AST blocks to support testing. + * `start` and `end` properties are omitted for brevity. + */ + +type TestNodeExcerpt = { + code: string; + node: N; +}; + +export const FunctionDeclarationFoo: TestNodeExcerpt = { + code: 'function foo() {}', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, +}; + +export const ConstFooAssignedFunctionExpression: TestNodeExcerpt = { + code: 'const foo = function() {}', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'foo', + }, + init: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + ], + }, +}; + +export const AssignmentExpressionOfArrowFunctionToFooIdentifier: TestNodeExcerpt = { + code: 'foo = () => {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'foo', + }, + right: { + type: 'ArrowFunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const AssignmentExpressionOfNamedFunctionToFooMemberExpression: TestNodeExcerpt = { + code: 'obj.foo = function bar() {}', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'a', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + right: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + }, + }, +}; + +export const MethodDefinitionOfFooInClassBar: TestNodeExcerpt = { + code: 'class Bar { foo() {} }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'Bar', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'MethodDefinition', + key: { + type: 'Identifier', + name: 'foo', + }, + value: { + type: 'FunctionExpression', + id: null, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [], + }, + }, + kind: 'method', + computed: false, + static: false, + }, + ], + }, + }, +}; + +export const SimpleCallExpressionOfFoo: TestNodeExcerpt = { + code: 'foo()', + node: { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, +}; + +export const SyncFunctionDeclarationWithAsyncCallExpression: TestNodeExcerpt = { + // NOTE: this is invalid syntax, it won't be parsed by acorn + // but it can be an intermediary state of the AST after we run + // `wrapWithAwait` on "bar" call expressions, for instance + code: 'function foo() { return () => await bar() }', + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'foo', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'ReturnStatement', + argument: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'AwaitExpression', + argument: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'bar', + }, + arguments: [], + optional: false, + }, + }, + }, + }, + ], + }, + }, +}; + +export const AssignmentOfFooToBar: TestNodeExcerpt = { + code: 'bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'Identifier', + name: 'bar', + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarMemberExpression: TestNodeExcerpt = { + code: 'obj.bar = foo', + node: { + type: 'ExpressionStatement', + expression: { + type: 'AssignmentExpression', + operator: '=', + left: { + type: 'MemberExpression', + computed: false, + optional: false, + object: { + type: 'Identifier', + name: 'obj', + }, + property: { + type: 'Identifier', + name: 'bar', + }, + }, + right: { + type: 'Identifier', + name: 'foo', + }, + }, + }, +}; + +export const AssignmentOfFooToBarVariableDeclarator: TestNodeExcerpt = { + code: 'const bar = foo', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, +}; + +export const AssignmentOfFooToBarPropertyDefinition: TestNodeExcerpt = { + code: 'class baz { bar = foo }', + node: { + type: 'ClassDeclaration', + id: { + type: 'Identifier', + name: 'baz', + }, + superClass: null, + body: { + type: 'ClassBody', + body: [ + { + type: 'PropertyDefinition', + static: false, + computed: false, + key: { + type: 'Identifier', + name: 'bar', + }, + value: { + type: 'Identifier', + name: 'foo', + }, + }, + ], + }, + }, +}; + +const fixSimpleCallExpressionCode = ` +function bar() { + const a = foo(); + + return a; +}`; + +export const FixSimpleCallExpression: TestNodeExcerpt = { + code: fixSimpleCallExpressionCode, + node: { + type: 'FunctionDeclaration', + id: { + type: 'Identifier', + name: 'bar', + }, + expression: false, + generator: false, + async: false, + params: [], + body: { + type: 'BlockStatement', + body: [ + { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'a', + }, + init: { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'foo', + }, + arguments: [], + optional: false, + }, + }, + ], + }, + { + type: 'ReturnStatement', + argument: { + type: 'Identifier', + name: 'a', + }, + }, + ], + }, + }, +}; + +export const ArrowFunctionDerefCallExpression: TestNodeExcerpt = { + // NOTE: this call strategy is widely used by bundlers; it's used to sever the `this` + // reference in the method from the object that contains it. This is mostly because + // the bundler wants to ensure that it does not messes up the bindings in the code it + // generates. + // + // This would be similar to doing `foo.call(undefined)` + code: 'const bar = () => (0, e.foo)();', + node: { + type: 'VariableDeclaration', + kind: 'const', + declarations: [ + { + type: 'VariableDeclarator', + id: { + type: 'Identifier', + name: 'bar', + }, + init: { + type: 'ArrowFunctionExpression', + id: null, + expression: true, + generator: false, + async: false, + params: [], + body: { + type: 'CallExpression', + optional: false, + arguments: [], + callee: { + type: 'SequenceExpression', + expressions: [ + { + type: 'Literal', + value: 0, + }, + { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'e', + }, + property: { + type: 'Identifier', + name: 'foo', + }, + computed: false, + optional: false, + }, + ], + }, + }, + }, + }, + ], + }, +}; diff --git a/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts b/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts new file mode 100644 index 000000000000..2b00c271f730 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/ast/tests/operations.test.ts @@ -0,0 +1,245 @@ +import { assertEquals, assertThrows } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; + +import { WalkerState, asyncifyScope, buildFixModifiedFunctionsOperation, checkReassignmentOfModifiedIdentifiers, getFunctionIdentifier, wrapWithAwait } from '../operations.ts'; +import { + ArrowFunctionDerefCallExpression, + AssignmentExpressionOfArrowFunctionToFooIdentifier, + AssignmentExpressionOfNamedFunctionToFooMemberExpression, + AssignmentOfFooToBar, + AssignmentOfFooToBarMemberExpression, + AssignmentOfFooToBarPropertyDefinition, + AssignmentOfFooToBarVariableDeclarator, + ConstFooAssignedFunctionExpression, + FixSimpleCallExpression, + FunctionDeclarationFoo, + MethodDefinitionOfFooInClassBar, + SimpleCallExpressionOfFoo, + SyncFunctionDeclarationWithAsyncCallExpression, +} from './data/ast_blocks.ts'; +import { AnyNode, ArrowFunctionExpression, AssignmentExpression, AwaitExpression, Expression, MethodDefinition, ReturnStatement, VariableDeclaration } from '../../../acorn.d.ts'; +import { assertNotEquals } from 'https://deno.land/std@0.203.0/assert/assert_not_equals.ts'; + +describe('getFunctionIdentifier', () => { + it(`identifies the name "foo" for the code \`${FunctionDeclarationFoo.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [FunctionDeclarationFoo.node]; + const functionNodeIndex = 0; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${ConstFooAssignedFunctionExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + ConstFooAssignedFunctionExpression.node, // VariableDeclaration + ConstFooAssignedFunctionExpression.node.declarations[0], // VariableDeclarator + ConstFooAssignedFunctionExpression.node.declarations[0].init! // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfArrowFunctionToFooIdentifier.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfArrowFunctionToFooIdentifier.node, // ExpressionStatement + AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression, // AssignmentExpression + (AssignmentExpressionOfArrowFunctionToFooIdentifier.node.expression as AssignmentExpression).right, // ArrowFunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${AssignmentExpressionOfNamedFunctionToFooMemberExpression.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node, // ExpressionStatement + AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression, // AssignmentExpression + (AssignmentExpressionOfNamedFunctionToFooMemberExpression.node.expression as AssignmentExpression).right, // FunctionExpression + ]; + const functionNodeIndex = 2; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); + + it(`identifies the name "foo" for the code \`${MethodDefinitionOfFooInClassBar.code}\``, () => { + // ancestors array is built by the walking lib + const nodeAncestors = [ + MethodDefinitionOfFooInClassBar.node, // ClassDeclaration + MethodDefinitionOfFooInClassBar.node.body, // ClassBody + MethodDefinitionOfFooInClassBar.node.body!.body[0], // MethodDefinition + (MethodDefinitionOfFooInClassBar.node.body!.body[0] as MethodDefinition).value, // FunctionExpression + ]; + const functionNodeIndex = 3; + assertEquals('foo', getFunctionIdentifier(nodeAncestors, functionNodeIndex)); + }); +}); + +describe('wrapWithAwait', () => { + it('wraps a call expression with await', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node.expression); + wrapWithAwait(node); + + assertEquals('AwaitExpression', node.type); + assertNotEquals(SimpleCallExpressionOfFoo.node.expression.type, node.type); + assertEquals(SimpleCallExpressionOfFoo.node.expression, (node as AwaitExpression).argument); + }); + + it('throws if node is not an expression', () => { + const node = structuredClone(SimpleCallExpressionOfFoo.node); + assertThrows(() => wrapWithAwait(node as unknown as Expression)); + }) +}); + +describe('asyncifyScope', () => { + it('makes only the first function scope async', () => { + const node = structuredClone(SyncFunctionDeclarationWithAsyncCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body!.body[0], // ReturnStatement + (node.body!.body[0] as ReturnStatement).argument!, // ArrowFunctionExpression + ((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body, // AwaitExpression + (((node.body!.body[0] as ReturnStatement).argument! as ArrowFunctionExpression).body as AwaitExpression).argument, // CallExpression + ]; + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(), + } + + asyncifyScope(ancestors, state); + + // Assert the function did indeed change the expression to async + assertEquals(((node.body.body[0] as ReturnStatement).argument as ArrowFunctionExpression).async, true) + + // Assert the function did NOT change all ancestors in the chain + assertEquals(node.async, false); + + // Assert it couldn't find a function identifier + assertEquals(state.functionIdentifiers.size, 0); + }); +}); + +describe('checkReassignmentofModifiedIdentifiers', () => { + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBar.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBar.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarMemberExpression.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarMemberExpression.node); + const ancestors: AnyNode[] = [ + node, // ExpressionStatement + node.expression, // AssignmentExpression + (node.expression as AssignmentExpression).right, // Identifier + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.expression, state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarVariableDeclarator.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarVariableDeclarator.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.declarations[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); + + it(`identifies the reassignment of "foo" in the code "${AssignmentOfFooToBarPropertyDefinition.code}"`, () => { + const node = structuredClone(AssignmentOfFooToBarPropertyDefinition.node); + const ancestors: AnyNode[] = [ + node, // ClassDeclaration + node.body, // ClassBody + node.body.body[0], // PropertyDefinition + ]; + const state: WalkerState = { + isModified: true, + functionIdentifiers: new Set(['foo']), + } + + checkReassignmentOfModifiedIdentifiers(node.body.body[0], state, ancestors, ''); + + assertEquals(state.functionIdentifiers.has('bar'), true); + }); +}); + +describe('buildFixModifiedFunctionsOperation', function() { + const state: WalkerState = { + isModified: false, + functionIdentifiers: new Set(['foo']), + }; + + const fixFunction = buildFixModifiedFunctionsOperation(state.functionIdentifiers); + + beforeEach(() => { + state.isModified = false; + state.functionIdentifiers = new Set(['foo']); + }); + + it(`fixes calls of "foo" in the code "${FixSimpleCallExpression.code}"`, () => { + const node = structuredClone(FixSimpleCallExpression.node); + const ancestors: AnyNode[] = [ + node, // FunctionDeclaration + node.body, // BlockStatement + node.body.body[0], // VariableDeclaration + (node.body.body[0] as VariableDeclaration).declarations[0], // VariableDeclarator + (node.body.body[0] as VariableDeclaration).declarations[0].init!, // CallExpression + ]; + + fixFunction(ancestors[4], state, ancestors, ''); + + assertEquals(state.isModified, true); + assertEquals(state.functionIdentifiers.has('bar'), true); + assertNotEquals(FixSimpleCallExpression.node, node); + assertEquals(node.async, true); + assertEquals(ancestors[4].type, 'AwaitExpression'); + }); + + it(`fixes calls of "foo" in the code "${ArrowFunctionDerefCallExpression.code}"`, () => { + const node = structuredClone(ArrowFunctionDerefCallExpression.node); + const ancestors: AnyNode[] = [ + node, // VariableDeclaration + node.declarations[0], // VariableDeclarator + node.declarations[0].init!, // ArrowFunctionExpression + (node.declarations[0].init as ArrowFunctionExpression).body, // CallExpression + ]; + + fixFunction(ancestors[3], state, ancestors, ''); + + // Recorded that a modification has been made + assertEquals(state.isModified, true); + // Recorded that the enclosing scope of the call also requires fixing + assertEquals(state.functionIdentifiers.has('bar'), true); + // Original node and fixed node are different + assertNotEquals(ArrowFunctionDerefCallExpression.node, node); + // The function call is now await'ed + assertEquals(ancestors[3].type, 'AwaitExpression'); + // The parent function of the call is now marked as async + assertEquals((ancestors[2] as ArrowFunctionExpression).async, true); + }); +}) diff --git a/packages/apps-engine/deno-runtime/lib/codec.ts b/packages/apps-engine/deno-runtime/lib/codec.ts new file mode 100644 index 000000000000..288db46169dc --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/codec.ts @@ -0,0 +1,43 @@ +import { Buffer } from 'node:buffer'; +import { Decoder, Encoder, ExtensionCodec } from '@msgpack/msgpack'; + +import type { App as _App } from '@rocket.chat/apps-engine/definition/App.ts'; +import { require } from "./require.ts"; + +const { App } = require('@rocket.chat/apps-engine/definition/App.js') as { + App: typeof _App; +}; + +const extensionCodec = new ExtensionCodec(); + +extensionCodec.register({ + type: 0, + encode: (object: unknown) => { + // We don't care about functions, but also don't want to throw an error + if (typeof object === 'function' || object instanceof App) { + return new Uint8Array(0); + } + + return null; + }, + decode: (_data: Uint8Array) => undefined, +}); + +// Since Deno doesn't have Buffer by default, we need to use Uint8Array +extensionCodec.register({ + type: 1, + encode: (object: unknown) => { + if (object instanceof Buffer) { + return new Uint8Array(object.buffer, object.byteOffset, object.byteLength); + } + + return null; + }, + // msgpack will reuse the Uint8Array instance, so WE NEED to copy it instead of simply creating a view + decode: (data: Uint8Array) => { + return Buffer.from(data); + }, +}); + +export const encoder = new Encoder({ extensionCodec }); +export const decoder = new Decoder({ extensionCodec }); diff --git a/packages/apps-engine/deno-runtime/lib/logger.ts b/packages/apps-engine/deno-runtime/lib/logger.ts new file mode 100644 index 000000000000..ea2701c70230 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/logger.ts @@ -0,0 +1,142 @@ +import stackTrace from 'stack-trace'; +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; + +export interface StackFrame { + getTypeName(): string; + getFunctionName(): string; + getMethodName(): string; + getFileName(): string; + getLineNumber(): number; + getColumnNumber(): number; + isNative(): boolean; + isConstructor(): boolean; +} + +enum LogMessageSeverity { + DEBUG = 'debug', + INFORMATION = 'info', + LOG = 'log', + WARNING = 'warning', + ERROR = 'error', + SUCCESS = 'success', +} + +type Entry = { + caller: string; + severity: LogMessageSeverity; + method: string; + timestamp: Date; + args: Array; +}; + +interface ILoggerStorageEntry { + appId: string; + method: string; + entries: Array; + startTime: Date; + endTime: Date; + totalTime: number; + _createdAt: Date; +} + +export class Logger { + private entries: Array; + private start: Date; + private method: string; + + constructor(method: string) { + this.method = method; + this.entries = []; + this.start = new Date(); + } + + public debug(...args: Array): void { + this.addEntry(LogMessageSeverity.DEBUG, this.getStack(stackTrace.get()), ...args); + } + + public info(...args: Array): void { + this.addEntry(LogMessageSeverity.INFORMATION, this.getStack(stackTrace.get()), ...args); + } + + public log(...args: Array): void { + this.addEntry(LogMessageSeverity.LOG, this.getStack(stackTrace.get()), ...args); + } + + public warn(...args: Array): void { + this.addEntry(LogMessageSeverity.WARNING, this.getStack(stackTrace.get()), ...args); + } + + public error(...args: Array): void { + this.addEntry(LogMessageSeverity.ERROR, this.getStack(stackTrace.get()), ...args); + } + + public success(...args: Array): void { + this.addEntry(LogMessageSeverity.SUCCESS, this.getStack(stackTrace.get()), ...args); + } + + private addEntry(severity: LogMessageSeverity, caller: string, ...items: Array): void { + const i = items.map((args) => { + if (args instanceof Error) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'stack' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + if (typeof args === 'object' && args !== null && 'message' in args) { + return JSON.stringify(args, Object.getOwnPropertyNames(args)); + } + const str = JSON.stringify(args, null, 2); + return str ? JSON.parse(str) : str; // force call toJSON to prevent circular references + }); + + this.entries.push({ + caller, + severity, + method: this.method, + timestamp: new Date(), + args: i, + }); + } + + private getStack(stack: Array): string { + let func = 'anonymous'; + + if (stack.length === 1) { + return func; + } + + const frame = stack[1]; + + if (frame.getMethodName() === null) { + func = 'anonymous OR constructor'; + } else { + func = frame.getMethodName(); + } + + if (frame.getFunctionName() !== null) { + func = `${func} -> ${frame.getFunctionName()}`; + } + + return func; + } + + private getTotalTime(): number { + return new Date().getTime() - this.start.getTime(); + } + + public hasEntries(): boolean { + return this.entries.length > 0; + } + + public getLogs(): ILoggerStorageEntry { + return { + appId: AppObjectRegistry.get('id')!, + method: this.method, + entries: this.entries, + startTime: this.start, + endTime: new Date(), + totalTime: this.getTotalTime(), + _createdAt: new Date(), + }; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/messenger.ts b/packages/apps-engine/deno-runtime/lib/messenger.ts new file mode 100644 index 000000000000..1e9ffe05c6c5 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/messenger.ts @@ -0,0 +1,199 @@ +import { writeAll } from "https://deno.land/std@0.216.0/io/write_all.ts"; + +import * as jsonrpc from 'jsonrpc-lite'; + +import { AppObjectRegistry } from '../AppObjectRegistry.ts'; +import type { Logger } from './logger.ts'; +import { encoder } from './codec.ts'; + +export type RequestDescriptor = Pick; + +export type NotificationDescriptor = Pick; + +export type SuccessResponseDescriptor = Pick; + +export type ErrorResponseDescriptor = Pick; + +export type JsonRpcRequest = jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification; +export type JsonRpcResponse = jsonrpc.IParsedObjectSuccess | jsonrpc.IParsedObjectError; + +export function isRequest(message: jsonrpc.IParsedObject): message is JsonRpcRequest { + return message.type === 'request' || message.type === 'notification'; +} + +export function isResponse(message: jsonrpc.IParsedObject): message is JsonRpcResponse { + return message.type === 'success' || message.type === 'error'; +} + +export function isErrorResponse(message: jsonrpc.JsonRpc): message is jsonrpc.ErrorObject { + return message instanceof jsonrpc.ErrorObject; +} + +const COMMAND_PONG = '_zPONG'; + +export const RPCResponseObserver = new EventTarget(); + +export const Queue = new (class Queue { + private queue: Uint8Array[] = []; + private isProcessing = false; + + private async processQueue() { + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + + while (this.queue.length) { + const message = this.queue.shift(); + + if (message) { + await Transport.send(message); + } + } + + this.isProcessing = false; + } + + public enqueue(message: jsonrpc.JsonRpc | typeof COMMAND_PONG) { + this.queue.push(encoder.encode(message)); + this.processQueue(); + } +}); + +export const Transport = new (class Transporter { + private selectedTransport: Transporter['stdoutTransport'] | Transporter['noopTransport']; + + constructor() { + this.selectedTransport = this.stdoutTransport.bind(this); + } + + private async stdoutTransport(message: Uint8Array): Promise { + await writeAll(Deno.stdout, message); + } + + private async noopTransport(_message: Uint8Array): Promise {} + + public selectTransport(transport: 'stdout' | 'noop'): void { + switch (transport) { + case 'stdout': + this.selectedTransport = this.stdoutTransport.bind(this); + break; + case 'noop': + this.selectedTransport = this.noopTransport.bind(this); + break; + } + } + + public send(message: Uint8Array): Promise { + return this.selectedTransport(message); + } +})(); + +export function parseMessage(message: string | Record) { + let parsed: jsonrpc.IParsedObject | jsonrpc.IParsedObject[]; + + if (typeof message === 'string') { + parsed = jsonrpc.parse(message); + } else { + parsed = jsonrpc.parseObject(message); + } + + if (Array.isArray(parsed)) { + throw jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + } + + if (parsed.type === 'invalid') { + throw jsonrpc.error(null, parsed.payload); + } + + return parsed; +} + +export async function sendInvalidRequestError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest(null)); + + await Queue.enqueue(rpc); +} + +export async function sendInvalidParamsError(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.invalidParams(null)); + + await Queue.enqueue(rpc); +} + +export async function sendParseError(): Promise { + const rpc = jsonrpc.error(null, jsonrpc.JsonRpcError.parseError(null)); + + await Queue.enqueue(rpc); +} + +export async function sendMethodNotFound(id: jsonrpc.ID): Promise { + const rpc = jsonrpc.error(id, jsonrpc.JsonRpcError.methodNotFound(null)); + + await Queue.enqueue(rpc); +} + +export async function errorResponse({ error: { message, code = -32000, data = {} }, id }: ErrorResponseDescriptor): Promise { + const logger = AppObjectRegistry.get('logger'); + + if (logger?.hasEntries()) { + data.logs = logger.getLogs(); + } + + const rpc = jsonrpc.error(id, new jsonrpc.JsonRpcError(message, code, data)); + + await Queue.enqueue(rpc); +} + +export async function successResponse({ id, result }: SuccessResponseDescriptor): Promise { + const payload = { value: result } as Record; + const logger = AppObjectRegistry.get('logger'); + + if (logger?.hasEntries()) { + payload.logs = logger.getLogs(); + } + + const rpc = jsonrpc.success(id, payload); + + await Queue.enqueue(rpc); +} + +export function pongResponse(): Promise { + return Promise.resolve(Queue.enqueue(COMMAND_PONG)); +} + +export async function sendRequest(requestDescriptor: RequestDescriptor): Promise { + const request = jsonrpc.request(Math.random().toString(36).slice(2), requestDescriptor.method, requestDescriptor.params); + + // TODO: add timeout to this + const responsePromise = new Promise((resolve, reject) => { + const handler = (event: Event) => { + if (event instanceof ErrorEvent) { + reject(event.error); + } + + if (event instanceof CustomEvent) { + resolve(event.detail); + } + + RPCResponseObserver.removeEventListener(`response:${request.id}`, handler); + }; + + RPCResponseObserver.addEventListener(`response:${request.id}`, handler); + }); + + await Queue.enqueue(request); + + return responsePromise as Promise; +} + +export function sendNotification({ method, params }: NotificationDescriptor) { + const request = jsonrpc.notification(method, params); + + Queue.enqueue(request); +} + +export function log(params: jsonrpc.RpcParams) { + sendNotification({ method: 'log', params }); +} diff --git a/packages/apps-engine/deno-runtime/lib/require.ts b/packages/apps-engine/deno-runtime/lib/require.ts new file mode 100644 index 000000000000..3288ecf67ffa --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/require.ts @@ -0,0 +1,14 @@ +import { createRequire } from 'node:module'; + +const _require = createRequire(import.meta.url); + +export const require = (mod: string) => { + // When we try to import something from the apps-engine, we resolve the path using import maps from Deno + // However, the import maps are configured to look at the source folder for typescript files, but during + // runtime those files are not available + if (mod.startsWith('@rocket.chat/apps-engine')) { + mod = import.meta.resolve(mod).replace('file://', '').replace('src/', ''); + } + + return _require(mod); +} diff --git a/packages/apps-engine/deno-runtime/lib/room.ts b/packages/apps-engine/deno-runtime/lib/room.ts new file mode 100644 index 000000000000..b7423cdd31ff --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/room.ts @@ -0,0 +1,104 @@ +import type { IRoom } from '@rocket.chat/apps-engine/definition/rooms/IRoom.ts'; +import type { RoomType } from '@rocket.chat/apps-engine/definition/rooms/RoomType.ts'; +import type { IUser } from '@rocket.chat/apps-engine/definition/users/IUser.ts'; +import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager.ts'; + +const PrivateManager = Symbol('RoomPrivateManager'); + +export class Room { + public id: string | undefined; + + public displayName?: string; + + public slugifiedName: string | undefined; + + public type: RoomType | undefined; + + public creator: IUser | undefined; + + public isDefault?: boolean; + + public isReadOnly?: boolean; + + public displaySystemMessages?: boolean; + + public messageCount?: number; + + public createdAt?: Date; + + public updatedAt?: Date; + + public lastModifiedAt?: Date; + + public customFields?: { [key: string]: unknown }; + + public userIds?: Array; + + private _USERNAMES: Promise> | undefined; + + private [PrivateManager]: AppManager | undefined; + + /** + * @deprecated + */ + public get usernames(): Promise> { + if (!this._USERNAMES) { + this._USERNAMES = this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || Promise.resolve([]); + } + + public set usernames(usernames) {} + + public constructor(room: IRoom, manager: AppManager) { + Object.assign(this, room); + + Object.defineProperty(this, PrivateManager, { + configurable: false, + enumerable: false, + writable: false, + value: manager, + }); + } + + get value(): object { + return { + id: this.id, + displayName: this.displayName, + slugifiedName: this.slugifiedName, + type: this.type, + creator: this.creator, + isDefault: this.isDefault, + isReadOnly: this.isReadOnly, + displaySystemMessages: this.displaySystemMessages, + messageCount: this.messageCount, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + lastModifiedAt: this.lastModifiedAt, + customFields: this.customFields, + userIds: this.userIds, + }; + } + + public async getUsernames(): Promise> { + // Get usernames + if (!this._USERNAMES) { + this._USERNAMES = await this[PrivateManager]?.getBridges().getInternalBridge().doGetUsernamesOfRoomById(this.id); + } + + return this._USERNAMES || []; + } + + public toJSON() { + return this.value; + } + + public toString() { + return this.value; + } + + public valueOf() { + return this.value; + } +} diff --git a/packages/apps-engine/deno-runtime/lib/roomFactory.ts b/packages/apps-engine/deno-runtime/lib/roomFactory.ts new file mode 100644 index 000000000000..8c270eeb86b9 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/roomFactory.ts @@ -0,0 +1,27 @@ +import type { IRoom } from "@rocket.chat/apps-engine/definition/rooms/IRoom.ts"; +import type { AppManager } from "@rocket.chat/apps-engine/server/AppManager.ts"; + +import { AppAccessors } from "./accessors/mod.ts"; +import { Room } from "./room.ts"; +import { JsonRpcError } from "jsonrpc-lite"; + +const getMockAppManager = (senderFn: AppAccessors['senderFn']) => ({ + getBridges: () => ({ + getInternalBridge: () => ({ + doGetUsernamesOfRoomById: (roomId: string) => { + return senderFn({ + method: 'bridges:getInternalBridge:doGetUsernamesOfRoomById', + params: [roomId], + }).then((result) => result.result).catch((err) => { + throw new JsonRpcError(`Error getting usernames of room: ${err}`, -32000); + }) + }, + }), + }), +}); + +export default function createRoom(room: IRoom, senderFn: AppAccessors['senderFn']) { + const mockAppManager = getMockAppManager(senderFn); + + return new Room(room, mockAppManager as unknown as AppManager); +} diff --git a/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts b/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts new file mode 100644 index 000000000000..91cf6587e741 --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/sanitizeDeprecatedUsage.ts @@ -0,0 +1,20 @@ +import { fixBrokenSynchronousAPICalls } from "./ast/mod.ts"; + +function hasPotentialDeprecatedUsage(source: string) { + return ( + // potential usage of Room.usernames getter + source.includes('.usernames') || + // potential usage of LivechatRead.isOnline method + source.includes('.isOnline(') || + // potential usage of LivechatCreator.createToken method + source.includes('.createToken(') + ) +} + +export function sanitizeDeprecatedUsage(source: string) { + if (!hasPotentialDeprecatedUsage(source)) { + return source; + } + + return fixBrokenSynchronousAPICalls(source); +} diff --git a/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts b/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts new file mode 100644 index 000000000000..f69a728e79af --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/tests/logger.test.ts @@ -0,0 +1,111 @@ +import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { Logger } from "../logger.ts"; + +describe('Logger', () => { + it('getLogs should return an array of entries', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.method, 'test'); + }) + + it('should be able to add entries of different severity', () => { + const logger = new Logger('test'); + logger.info('test'); + logger.debug('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 3); + assertEquals(logs.entries[0].severity, 'info'); + assertEquals(logs.entries[1].severity, 'debug'); + assertEquals(logs.entries[2].severity, 'error'); + }) + + it('should be able to add an info entry', () => { + const logger = new Logger('test'); + logger.info('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'info'); + }); + + it('should be able to add an debug entry', () => { + const logger = new Logger('test'); + logger.debug('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'debug'); + }); + + it('should be able to add an error entry', () => { + const logger = new Logger('test'); + logger.error('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'error'); + }); + + it('should be able to add an success entry', () => { + const logger = new Logger('test'); + logger.success('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'success'); + }); + + it('should be able to add an warning entry', () => { + const logger = new Logger('test'); + logger.warn('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'warning'); + }); + + it('should be able to add an log entry', () => { + const logger = new Logger('test'); + logger.log('test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments', () => { + const logger = new Logger('test'); + logger.log('test', 'test', 'test'); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 'test'); + assertEquals(logs.entries[0].args[2], 'test'); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + + it('should be able to add an entry with multiple arguments of different types', () => { + const logger = new Logger('test'); + logger.log('test', 1, true, { foo: 'bar' }); + const logs = logger.getLogs(); + assertEquals(logs.entries.length, 1); + assertEquals(logs.entries[0].args[0], 'test'); + assertEquals(logs.entries[0].args[1], 1); + assertEquals(logs.entries[0].args[2], true); + assertEquals(logs.entries[0].args[3], { foo: 'bar' }); + assertEquals(logs.entries[0].method, 'test'); + assertEquals(logs.entries[0].severity, 'log'); + }); + +}) diff --git a/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts b/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts new file mode 100644 index 000000000000..9b4f128380bc --- /dev/null +++ b/packages/apps-engine/deno-runtime/lib/tests/messenger.test.ts @@ -0,0 +1,96 @@ +import { assertEquals, assertObjectMatch } from 'https://deno.land/std@0.203.0/assert/mod.ts'; +import { afterAll, beforeEach, describe, it } from 'https://deno.land/std@0.203.0/testing/bdd.ts'; +import { spy } from 'https://deno.land/std@0.203.0/testing/mock.ts'; + +import * as Messenger from '../messenger.ts'; +import { AppObjectRegistry } from '../../AppObjectRegistry.ts'; +import { Logger } from '../logger.ts'; + +describe('Messenger', () => { + beforeEach(() => { + AppObjectRegistry.clear(); + AppObjectRegistry.set('logger', new Logger('test')); + AppObjectRegistry.set('id', 'test'); + Messenger.Transport.selectTransport('noop'); + }); + + afterAll(() => { + AppObjectRegistry.clear(); + Messenger.Transport.selectTransport('stdout'); + }); + + it('should add logs to success responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + + const logger = AppObjectRegistry.get('logger') as Logger; + + logger.info('test'); + + await Messenger.successResponse({ id: 'test', result: 'test' }); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument, { + jsonrpc: '2.0', + id: 'test', + result: { + value: 'test', + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }); + + theSpy.restore(); + }); + + it('should add logs to error responses', async () => { + const theSpy = spy(Messenger.Queue, 'enqueue'); + + const logger = AppObjectRegistry.get('logger') as Logger; + + logger.info('test'); + + await Messenger.errorResponse({ id: 'test', error: { code: -32000, message: 'test' } }); + + assertEquals(theSpy.calls.length, 1); + + const [responseArgument] = theSpy.calls[0].args; + + assertObjectMatch(responseArgument, { + jsonrpc: '2.0', + id: 'test', + error: { + code: -32000, + message: 'test', + data: { + logs: { + appId: 'test', + method: 'test', + entries: [ + { + severity: 'info', + method: 'test', + args: ['test'], + caller: 'anonymous OR constructor', + }, + ], + }, + }, + }, + }); + + theSpy.restore(); + }); +}); diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts new file mode 100644 index 000000000000..09be5258ecd0 --- /dev/null +++ b/packages/apps-engine/deno-runtime/main.ts @@ -0,0 +1,129 @@ +if (!Deno.args.includes('--subprocess')) { + Deno.stderr.writeSync( + new TextEncoder().encode(` + This is a Deno wrapper for Rocket.Chat Apps. It is not meant to be executed stand-alone; + It is instead meant to be executed as a subprocess by the Apps-Engine framework. + `), + ); + Deno.exit(1001); +} + +import { JsonRpcError } from 'jsonrpc-lite'; +import type { App } from '@rocket.chat/apps-engine/definition/App.ts'; + +import * as Messenger from './lib/messenger.ts'; +import { decoder } from './lib/codec.ts'; +import { AppObjectRegistry } from './AppObjectRegistry.ts'; +import { Logger } from './lib/logger.ts'; + +import slashcommandHandler from './handlers/slashcommand-handler.ts'; +import videoConferenceHandler from './handlers/videoconference-handler.ts'; +import apiHandler from './handlers/api-handler.ts'; +import handleApp from './handlers/app/handler.ts'; +import handleScheduler from './handlers/scheduler-handler.ts'; + +type Handlers = { + app: typeof handleApp; + api: typeof apiHandler; + slashcommand: typeof slashcommandHandler; + videoconference: typeof videoConferenceHandler; + scheduler: typeof handleScheduler; + ping: (method: string, params: unknown) => 'pong'; +}; + +const COMMAND_PING = '_zPING'; + +async function requestRouter({ type, payload }: Messenger.JsonRpcRequest): Promise { + const methodHandlers: Handlers = { + app: handleApp, + api: apiHandler, + slashcommand: slashcommandHandler, + videoconference: videoConferenceHandler, + scheduler: handleScheduler, + ping: (_method, _params) => 'pong', + }; + + // We're not handling notifications at the moment + if (type === 'notification') { + return Messenger.sendInvalidRequestError(); + } + + const { id, method, params } = payload; + + const logger = new Logger(method); + AppObjectRegistry.set('logger', logger); + + const app = AppObjectRegistry.get('app'); + + if (app) { + // Same logic as applied in the ProxiedApp class previously + (app as unknown as Record).logger = logger; + } + + const [methodPrefix] = method.split(':') as [keyof Handlers]; + const handler = methodHandlers[methodPrefix]; + + if (!handler) { + return Messenger.errorResponse({ + error: { message: 'Method not found', code: -32601 }, + id, + }); + } + + const result = await handler(method, params); + + if (result instanceof JsonRpcError) { + return Messenger.errorResponse({ id, error: result }); + } + + return Messenger.successResponse({ id, result }); +} + +function handleResponse(response: Messenger.JsonRpcResponse): void { + let event: Event; + + if (response.type === 'error') { + event = new ErrorEvent(`response:${response.payload.id}`, { + error: response.payload, + }); + } else { + event = new CustomEvent(`response:${response.payload.id}`, { + detail: response.payload, + }); + } + + Messenger.RPCResponseObserver.dispatchEvent(event); +} + +async function main() { + Messenger.sendNotification({ method: 'ready' }); + + for await (const message of decoder.decodeStream(Deno.stdin.readable)) { + try { + // Process PING command first as it is not JSON RPC + if (message === COMMAND_PING) { + Messenger.pongResponse(); + continue; + } + + const JSONRPCMessage = Messenger.parseMessage(message as Record); + + if (Messenger.isRequest(JSONRPCMessage)) { + void requestRouter(JSONRPCMessage); + continue; + } + + if (Messenger.isResponse(JSONRPCMessage)) { + handleResponse(JSONRPCMessage); + } + } catch (error) { + if (Messenger.isErrorResponse(error)) { + await Messenger.errorResponse(error); + } else { + await Messenger.sendParseError(); + } + } + } +} + +main(); diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json new file mode 100644 index 000000000000..c229ff447000 --- /dev/null +++ b/packages/apps-engine/package.json @@ -0,0 +1,132 @@ +{ + "name": "@rocket.chat/apps-engine", + "version": "1.47.0-alpha", + "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", + "main": "index", + "typings": "index", + "scripts": { + "start": "run-s .:build:clean .:build:watch", + "testunit": "run-p .:test:node .:test:deno", + ".:test:node": "NODE_ENV=test ts-node ./tests/runner.ts", + ".:test:deno": "cd deno-runtime && deno task test", + "lint": "run-p .:lint:eslint .:lint:deno", + ".:lint:eslint": "eslint .", + ".:lint:deno": "deno lint --ignore=deno-runtime/.deno deno-runtime/", + "fix-lint": "eslint . --fix", + "build": "run-s .:build:clean .:build:default .:build:deno-cache", + ".:build:clean": "rimraf client definition server", + ".:build:default": "tsc -p tsconfig.json", + ".:build:deno-cache": "node scripts/deno-cache.js", + ".:build:watch": "yarn .:build:default --watch", + "typecheck": "tsc -p tsconfig.json --noEmit", + "bundle": "node scripts/bundle.js", + "gen-doc": "typedoc", + "prepack": "yarn bundle" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/RocketChat/Rocket.Chat.Apps-engine.git" + }, + "keywords": [ + "rocket.chat", + "team chat", + "apps engine" + ], + "files": [ + "client/**", + "definition/**", + "deno-runtime/**", + "lib/**", + "scripts/**", + "server/**" + ], + "publishConfig": { + "access": "public" + }, + "author": { + "name": "Rocket.Chat", + "url": "https://rocket.chat/" + }, + "contributors": [ + { + "name": "Bradley Hilton", + "email": "bradley.hilton@rocket.chat" + }, + { + "name": "Rodrigo Nascimento", + "email": "rodrigo.nascimento@rocket.chat" + }, + { + "name": "Douglas Gubert", + "email": "douglas.gubert@rocket.chat" + } + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/RocketChat/Rocket.Chat.Apps-engine/issues" + }, + "homepage": "https://github.com/RocketChat/Rocket.Chat.Apps-engine#readme", + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:~", + "@rocket.chat/ui-kit": "workspace:~", + "@types/adm-zip": "^0.5.0", + "@types/debug": "^4.1.12", + "@types/lodash.clonedeep": "^4.5.7", + "@types/nedb": "^1.8.12", + "@types/node": "^18.0.0", + "@types/semver": "^5.5.0", + "@types/stack-trace": "0.0.29", + "@types/uuid": "~8.3.4", + "@typescript-eslint/eslint-plugin": "~5.60.1", + "@typescript-eslint/parser": "~5.60.1", + "alsatian": "^2.4.0", + "browserify": "^16.5.2", + "eslint": "~8.45.0", + "nedb": "^1.8.0", + "npm-run-all": "^4.1.5", + "nyc": "^14.1.1", + "rimraf": "^6.0.1", + "tap-bark": "^1.0.0", + "ts-node": "^6.2.0", + "typedoc": "~0.24.8", + "typescript": "~5.1.6", + "uglify-es": "^3.3.9" + }, + "dependencies": { + "@msgpack/msgpack": "3.0.0-beta2", + "adm-zip": "^0.5.9", + "cryptiles": "^4.1.3", + "debug": "^4.3.4", + "esbuild": "^0.20.2", + "jose": "^4.11.1", + "jsonrpc-lite": "^2.2.0", + "lodash.clonedeep": "^4.5.0", + "semver": "^5.7.1", + "stack-trace": "0.0.10", + "uuid": "~8.3.2" + }, + "peerDependencies": { + "@rocket.chat/ui-kit": "workspace:^" + }, + "nyc": { + "include": [ + "src/*.ts", + "src/server/**/*.ts" + ], + "extension": [ + ".ts" + ], + "reporter": [ + "lcov", + "json", + "html" + ], + "all": true + }, + "volta": { + "extends": "../../package.json" + }, + "installConfig": { + "hoistingLimits": "workspaces" + } +} diff --git a/packages/apps-engine/scripts/bundle.js b/packages/apps-engine/scripts/bundle.js new file mode 100644 index 000000000000..a7f8932bec12 --- /dev/null +++ b/packages/apps-engine/scripts/bundle.js @@ -0,0 +1,35 @@ +const fs = require('fs'); +const path = require('path'); +const { Readable } = require('stream'); + +const browserify = require('browserify'); +const { minify } = require('uglify-es'); + +const targetDir = path.join(__dirname, '..', 'client'); + +// browserify accepts either a stream or a file path +const glue = new Readable({ + read() { + console.log('read'); + this.push("window.AppsEngineUIClient = require('./AppsEngineUIClient').AppsEngineUIClient;"); + this.push(null); + }, +}); + +async function main() { + const bundle = await new Promise((resolve, reject) => + browserify(glue, { + basedir: targetDir, + }).bundle((err, bundle) => { + if (err) return reject(err); + + resolve(bundle.toString()); + }), + ); + + const result = minify(bundle); + + fs.writeFileSync(path.join(targetDir, 'AppsEngineUIClient.min.js'), result.code); +} + +main(); diff --git a/packages/apps-engine/scripts/deno-cache.js b/packages/apps-engine/scripts/deno-cache.js new file mode 100644 index 000000000000..acf2f4b977b4 --- /dev/null +++ b/packages/apps-engine/scripts/deno-cache.js @@ -0,0 +1,25 @@ +const childProcess = require('child_process'); +const path = require('path'); + +try { + childProcess.execSync('deno info'); +} catch (e) { + console.error( + 'Could not execute "deno" in the system. It is now a requirement for the Apps-Engine framework, and Rocket.Chat apps will not work without it.\n', + 'Make sure to install Deno and run the installation process for the Apps-Engine again. More info on https://docs.deno.com/runtime/manual/getting_started/installation', + ); + process.exit(1); +} + +const rootPath = path.join(__dirname, '..'); +const denoRuntimePath = path.join(rootPath, 'deno-runtime'); +const DENO_DIR = process.env.DENO_DIR ?? path.join(rootPath, '.deno-cache'); + +childProcess.execSync('deno cache main.ts', { + cwd: denoRuntimePath, + env: { + DENO_DIR, + PATH: process.env.PATH, + }, + stdio: 'inherit', +}); diff --git a/packages/apps-engine/src/client/AppClientManager.ts b/packages/apps-engine/src/client/AppClientManager.ts new file mode 100644 index 000000000000..5d409f148f5f --- /dev/null +++ b/packages/apps-engine/src/client/AppClientManager.ts @@ -0,0 +1,28 @@ +import type { IAppInfo } from '../definition/metadata'; +import { AppServerCommunicator } from './AppServerCommunicator'; +import { AppsEngineUIHost } from './AppsEngineUIHost'; + +export class AppClientManager { + private apps: Array; + + constructor(private readonly appsEngineUIHost: AppsEngineUIHost, private readonly communicator?: AppServerCommunicator) { + if (!(appsEngineUIHost instanceof AppsEngineUIHost)) { + throw new Error('The appClientUIHost must extend appClientUIHost'); + } + + if (communicator && !(communicator instanceof AppServerCommunicator)) { + throw new Error('The communicator must extend AppServerCommunicator'); + } + + this.apps = []; + } + + public async load(): Promise { + this.apps = await this.communicator.getEnabledApps(); + console.log('Enabled apps:', this.apps); + } + + public async initialize(): Promise { + this.appsEngineUIHost.initialize(); + } +} diff --git a/packages/apps-engine/src/client/AppServerCommunicator.ts b/packages/apps-engine/src/client/AppServerCommunicator.ts new file mode 100644 index 000000000000..fae400bc7ff7 --- /dev/null +++ b/packages/apps-engine/src/client/AppServerCommunicator.ts @@ -0,0 +1,16 @@ +import type { IAppInfo } from '../definition/metadata'; + +export abstract class AppServerCommunicator { + public abstract getEnabledApps(): Promise>; + + public abstract getDisabledApps(): Promise>; + + // Map> + public abstract getLanguageAdditions(): Promise>>; + + // Map> + public abstract getSlashCommands(): Promise>>; + + // Map> + public abstract getContextualBarButtons(): Promise>>; +} diff --git a/packages/apps-engine/src/client/AppsEngineUIClient.ts b/packages/apps-engine/src/client/AppsEngineUIClient.ts new file mode 100644 index 000000000000..620be5e21d01 --- /dev/null +++ b/packages/apps-engine/src/client/AppsEngineUIClient.ts @@ -0,0 +1,70 @@ +import { ACTION_ID_LENGTH, MESSAGE_ID } from './constants'; +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; +import { AppsEngineUIMethods } from './definition/AppsEngineUIMethods'; +import { randomString } from './utils'; + +/** + * Represents the SDK provided to the external component. + */ +export class AppsEngineUIClient { + private listener: (this: Window, ev: MessageEvent) => any; + + private callbacks: Map any>; + + constructor() { + this.listener = () => console.log('init'); + this.callbacks = new Map(); + } + + /** + * Get the current user's information. + * + * @return the information of the current user. + */ + public getUserInfo(): Promise { + return this.call(AppsEngineUIMethods.GET_USER_INFO); + } + + /** + * Get the current room's information. + * + * @return the information of the current room. + */ + public getRoomInfo(): Promise { + return this.call(AppsEngineUIMethods.GET_ROOM_INFO); + } + + /** + * Initialize the app SDK for communicating with Rocket.Chat + */ + public init(): void { + this.listener = ({ data }) => { + if (!data?.hasOwnProperty(MESSAGE_ID)) { + return; + } + + const { + [MESSAGE_ID]: { id, payload }, + } = data; + + if (this.callbacks.has(id)) { + const resolve = this.callbacks.get(id); + + if (typeof resolve === 'function') { + resolve(payload); + } + this.callbacks.delete(id); + } + }; + window.addEventListener('message', this.listener); + } + + private call(action: string, payload?: any): Promise { + return new Promise((resolve) => { + const id = randomString(ACTION_ID_LENGTH); + + window.parent.postMessage({ [MESSAGE_ID]: { action, payload, id } }, '*'); + this.callbacks.set(id, resolve); + }); + } +} diff --git a/packages/apps-engine/src/client/AppsEngineUIHost.ts b/packages/apps-engine/src/client/AppsEngineUIHost.ts new file mode 100644 index 000000000000..02f82f236ed2 --- /dev/null +++ b/packages/apps-engine/src/client/AppsEngineUIHost.ts @@ -0,0 +1,78 @@ +import { MESSAGE_ID } from './constants'; +import type { IAppsEngineUIResponse, IExternalComponentRoomInfo, IExternalComponentUserInfo } from './definition'; +import { AppsEngineUIMethods } from './definition'; + +type HandleActionData = IExternalComponentUserInfo | IExternalComponentRoomInfo; + +/** + * Represents the host which handles API calls from external components. + */ +export abstract class AppsEngineUIHost { + /** + * The message emitter who calling the API. + */ + private responseDestination!: Window; + + constructor() { + this.initialize(); + } + + /** + * initialize the AppClientUIHost by registering window `message` listener + */ + public initialize() { + window.addEventListener('message', async ({ data, source }) => { + if (!data?.hasOwnProperty(MESSAGE_ID)) { + return; + } + + this.responseDestination = source as Window; + + const { + [MESSAGE_ID]: { action, id }, + } = data; + + switch (action) { + case AppsEngineUIMethods.GET_USER_INFO: + this.handleAction(action, id, await this.getClientUserInfo()); + break; + case AppsEngineUIMethods.GET_ROOM_INFO: + this.handleAction(action, id, await this.getClientRoomInfo()); + break; + } + }); + } + + /** + * Get the current user's information. + */ + public abstract getClientUserInfo(): Promise; + + /** + * Get the opened room's information. + */ + public abstract getClientRoomInfo(): Promise; + + /** + * Handle the action sent from the external component. + * @param action the name of the action + * @param id the unique id of the API call + * @param data The data that will return to the caller + */ + private async handleAction(action: AppsEngineUIMethods, id: string, data: HandleActionData): Promise { + if (this.responseDestination instanceof MessagePort || this.responseDestination instanceof ServiceWorker) { + return; + } + + this.responseDestination.postMessage( + { + [MESSAGE_ID]: { + id, + action, + payload: data, + } as IAppsEngineUIResponse, + }, + '*', + ); + } +} diff --git a/packages/apps-engine/src/client/constants/index.ts b/packages/apps-engine/src/client/constants/index.ts new file mode 100644 index 000000000000..bd7f2e779ca1 --- /dev/null +++ b/packages/apps-engine/src/client/constants/index.ts @@ -0,0 +1,6 @@ +/** + * The id length of each action. + */ +export const ACTION_ID_LENGTH = 80; + +export const MESSAGE_ID = 'rc-apps-engine-ui'; diff --git a/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts b/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts new file mode 100644 index 000000000000..df150f9ce62b --- /dev/null +++ b/packages/apps-engine/src/client/definition/AppsEngineUIMethods.ts @@ -0,0 +1,7 @@ +/** + * The actions provided by the AppClientSDK. + */ +export enum AppsEngineUIMethods { + GET_USER_INFO = 'getUserInfo', + GET_ROOM_INFO = 'getRoomInfo', +} diff --git a/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts b/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts new file mode 100644 index 000000000000..dff7289226a4 --- /dev/null +++ b/packages/apps-engine/src/client/definition/IAppsEngineUIResponse.ts @@ -0,0 +1,19 @@ +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from './index'; + +/** + * The response to the AppClientSDK's API call. + */ +export interface IAppsEngineUIResponse { + /** + * The name of the action + */ + action: string; + /** + * The unique id of the API call + */ + id: string; + /** + * The data that will return to the caller + */ + payload: IExternalComponentUserInfo | IExternalComponentRoomInfo; +} diff --git a/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts b/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts new file mode 100644 index 000000000000..32ca449e9650 --- /dev/null +++ b/packages/apps-engine/src/client/definition/IExternalComponentRoomInfo.ts @@ -0,0 +1,16 @@ +import type { IRoom } from '../../definition/rooms'; +import type { IExternalComponentUserInfo } from './IExternalComponentUserInfo'; + +type ClientRoomInfo = Pick; + +/** + * Represents the room's information returned to the + * external component. + */ +export interface IExternalComponentRoomInfo extends ClientRoomInfo { + /** + * the list that contains all the users belonging + * to this room. + */ + members: Array; +} diff --git a/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts b/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts new file mode 100644 index 000000000000..9212f5b39876 --- /dev/null +++ b/packages/apps-engine/src/client/definition/IExternalComponentUserInfo.ts @@ -0,0 +1,14 @@ +import type { IUser } from '../../definition/users'; + +type ClientUserInfo = Pick; + +/** + * Represents the user's information returned to + * the external component. + */ +export interface IExternalComponentUserInfo extends ClientUserInfo { + /** + * the avatar URL of the Rocket.Chat user + */ + avatarUrl: string; +} diff --git a/packages/apps-engine/src/client/definition/index.ts b/packages/apps-engine/src/client/definition/index.ts new file mode 100644 index 000000000000..70a1fe884a6a --- /dev/null +++ b/packages/apps-engine/src/client/definition/index.ts @@ -0,0 +1,4 @@ +export * from './AppsEngineUIMethods'; +export * from './IExternalComponentUserInfo'; +export * from './IExternalComponentRoomInfo'; +export * from './IAppsEngineUIResponse'; diff --git a/packages/apps-engine/src/client/index.ts b/packages/apps-engine/src/client/index.ts new file mode 100644 index 000000000000..2ebfee0264d2 --- /dev/null +++ b/packages/apps-engine/src/client/index.ts @@ -0,0 +1,4 @@ +import { AppClientManager } from './AppClientManager'; +import { AppServerCommunicator } from './AppServerCommunicator'; + +export { AppClientManager, AppServerCommunicator }; diff --git a/packages/apps-engine/src/client/utils/index.ts b/packages/apps-engine/src/client/utils/index.ts new file mode 100644 index 000000000000..f5e851e7d50f --- /dev/null +++ b/packages/apps-engine/src/client/utils/index.ts @@ -0,0 +1,18 @@ +/** + * Generate a random string with the specified length. + * @param length the length for the generated random string. + */ +export function randomString(length: number): string { + const buffer: Array = []; + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + for (let i = 0; i < length; i++) { + buffer.push(chars[getRandomInt(chars.length)]); + } + + return buffer.join(''); +} + +function getRandomInt(max: number): number { + return Math.floor(Math.random() * Math.floor(max)); +} diff --git a/packages/apps-engine/src/definition/App.ts b/packages/apps-engine/src/definition/App.ts new file mode 100644 index 000000000000..0044f35134b6 --- /dev/null +++ b/packages/apps-engine/src/definition/App.ts @@ -0,0 +1,236 @@ +import { AppStatus } from './AppStatus'; +import type { IApp } from './IApp'; +import type { + IAppAccessors, + IAppInstallationContext, + IAppUninstallationContext, + IConfigurationExtend, + IConfigurationModify, + IEnvironmentRead, + IHttp, + ILogger, + IModify, + IPersistence, + IRead, + IAppUpdateContext, +} from './accessors'; +import type { IAppAuthorInfo } from './metadata/IAppAuthorInfo'; +import type { IAppInfo } from './metadata/IAppInfo'; +import type { ISetting } from './settings'; +import type { ISettingUpdateContext } from './settings/ISettingUpdateContext'; + +export abstract class App implements IApp { + private status: AppStatus = AppStatus.UNKNOWN; + + /** + * Create a new App, this is called whenever the server starts up and initiates the Apps. + * Note, your implementation of this class should call `super(name, id, version)` so we have it. + * Also, please use the `initialize()` method to do items instead of the constructor as the constructor + * *might* be called more than once but the `initialize()` will only be called once. + */ + public constructor(private readonly info: IAppInfo, private readonly logger: ILogger, private readonly accessors?: IAppAccessors) { + this.logger.debug( + `Constructed the App ${this.info.name} (${this.info.id})`, + `v${this.info.version} which depends on the API v${this.info.requiredApiVersion}!`, + `Created by ${this.info.author.name}`, + ); + + this.setStatus(AppStatus.CONSTRUCTED); + } + + public async getStatus(): Promise { + return this.status; + } + + /** + * Get the name of this App. + * + * @return {string} the name + */ + public getName(): string { + return this.info.name; + } + + /** + * Gets the sluggified name of this App. + * + * @return {string} the name slugged + */ + public getNameSlug(): string { + return this.info.nameSlug; + } + + /** + * Gets the username of this App's app user. + * + * @return {string} the username of the app user + * + * @deprecated This method will be removed in the next major version. + * Please use read.getUserReader().getAppUser() instead. + */ + public getAppUserUsername(): string { + return `${this.info.nameSlug}.bot`; + } + + /** + * Get the ID of this App, please see for how to obtain an ID for your App. + * + * @return {number} the ID + */ + public getID(): string { + return this.info.id; + } + + /** + * Get the version of this App, using http://semver.org/. + * + * @return {string} the version + */ + public getVersion(): string { + return this.info.version; + } + + /** + * Get the description of this App, mostly used to show to the clients/administrators. + * + * @return {string} the description + */ + public getDescription(): string { + return this.info.description; + } + + /** + * Gets the API Version which this App depends on (http://semver.org/). + * This property is used for the dependency injections. + * + * @return {string} the required api version + */ + public getRequiredApiVersion(): string { + return this.info.requiredApiVersion; + } + + /** + * Gets the information regarding the author/maintainer of this App. + * + * @return author information + */ + public getAuthorInfo(): IAppAuthorInfo { + return this.info.author; + } + + /** + * Gets the entirity of the App's information. + * + * @return App information + */ + public getInfo(): IAppInfo { + return this.info; + } + + /** + * Gets the ILogger instance for this App. + * + * @return the logger instance + */ + public getLogger(): ILogger { + return this.logger; + } + + public getAccessors(): IAppAccessors { + return this.accessors; + } + + /** + * Method which will be called when the App is initialized. This is the recommended place + * to add settings and slash commands. If an error is thrown, all commands will be unregistered. + */ + public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise { + await this.extendConfiguration(configurationExtend, environmentRead); + } + + /** + * Method which is called when this App is enabled and can be called several + * times during this instance's life time. Once after the `initialize()` is called, + * pending it doesn't throw an error, and then anytime the App is enabled by the user. + * If this method, `onEnable()`, returns false, then this App will not + * actually be enabled (ex: a setting isn't configured). + * + * @return whether the App should be enabled or not + */ + public async onEnable(environment: IEnvironmentRead, configurationModify: IConfigurationModify): Promise { + return true; + } + + /** + * Method which is called when this App is disabled and it can be called several times. + * If this App was enabled and then the user disabled it, this method will be called. + */ + public async onDisable(configurationModify: IConfigurationModify): Promise {} + + /** + * Method which is called when the App is uninstalled and it is called one single time. + * + * This method will NOT be called when an App is getting disabled manually, ONLY when + * it's being uninstalled from Rocket.Chat. + */ + public async onUninstall(context: IAppUninstallationContext, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise {} + + /** + * Method which is called when the App is installed and it is called one single time. + * + * This method is NOT called when the App is updated. + */ + public async onInstall(context: IAppInstallationContext, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise {} + + /** + * Method which is called when the App is updated and it is called one single time. + * + * This method is NOT called when the App is installed. + */ + public async onUpdate(context: IAppUpdateContext, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise {} + + /** + * Method which is called whenever a setting which belongs to this App has been updated + * by an external system and not this App itself. The setting passed is the newly updated one. + * + * @param setting the setting which was updated + * @param configurationModify the accessor to modifiy the system + * @param reader the reader accessor + * @param http an accessor to the outside world + */ + public async onSettingUpdated(setting: ISetting, configurationModify: IConfigurationModify, read: IRead, http: IHttp): Promise {} + + /** + * Method which is called before a setting which belongs to this App is going to be updated + * by an external system and not this App itself. The setting passed is the newly updated one. + * + * @param setting the setting which is going to be updated + * @param configurationModify the accessor to modifiy the system + * @param reader the reader accessor + * @param http an accessor to the outside world + */ + public async onPreSettingUpdate(context: ISettingUpdateContext, configurationModify: IConfigurationModify, read: IRead, http: IHttp): Promise { + return context.newSetting; + } + + /** + * Method will be called during initialization. It allows for adding custom configuration options and defaults + * @param configuration + */ + protected async extendConfiguration(configuration: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise {} + + /** + * Sets the status this App is now at, use only when 100% true (it's protected for a reason). + * + * @param status the new status of this App + */ + protected async setStatus(status: AppStatus): Promise { + this.logger.debug(`The status is now: ${status}`); + this.status = status; + } + + // Avoid leaking references if object is serialized (e.g. to be sent over IPC) + public toJSON(): Record { + return this.info; + } +} diff --git a/packages/apps-engine/src/definition/AppStatus.ts b/packages/apps-engine/src/definition/AppStatus.ts new file mode 100644 index 000000000000..31638a8d0f1c --- /dev/null +++ b/packages/apps-engine/src/definition/AppStatus.ts @@ -0,0 +1,65 @@ +export enum AppStatus { + /** The status is known, aka not been constructed the proper way. */ + UNKNOWN = 'unknown', + /** The App has been constructed but that's it. */ + CONSTRUCTED = 'constructed', + /** The App's `initialize()` was called and returned true. */ + INITIALIZED = 'initialized', + /** The App's `onEnable()` was called, returned true, and this was done automatically (system start up). */ + AUTO_ENABLED = 'auto_enabled', + /** The App's `onEnable()` was called, returned true, and this was done by the user such as installing a new one. */ + MANUALLY_ENABLED = 'manually_enabled', + /** + * The App was disabled due to an error while attempting to compile it. + * An attempt to enable it again will fail, as it needs to be updated. + */ + COMPILER_ERROR_DISABLED = 'compiler_error_disabled', + /** + * The App was disable due to its license being invalid + */ + INVALID_LICENSE_DISABLED = 'invalid_license_disabled', + /** + * The app was disabled due to an invalid installation or validation in its signature. + */ + INVALID_INSTALLATION_DISABLED = 'invalid_installation_disabled', + /** The App was disabled due to an unrecoverable error being thrown. */ + ERROR_DISABLED = 'error_disabled', + /** The App was manually disabled by a user. */ + MANUALLY_DISABLED = 'manually_disabled', + INVALID_SETTINGS_DISABLED = 'invalid_settings_disabled', + /** The App was disabled due to other circumstances. */ + DISABLED = 'disabled', +} + +export class AppStatusUtilsDef { + public isEnabled(status: AppStatus): boolean { + switch (status) { + case AppStatus.AUTO_ENABLED: + case AppStatus.MANUALLY_ENABLED: + return true; + default: + return false; + } + } + + public isDisabled(status: AppStatus): boolean { + switch (status) { + case AppStatus.COMPILER_ERROR_DISABLED: + case AppStatus.ERROR_DISABLED: + case AppStatus.MANUALLY_DISABLED: + case AppStatus.INVALID_SETTINGS_DISABLED: + case AppStatus.INVALID_LICENSE_DISABLED: + case AppStatus.INVALID_INSTALLATION_DISABLED: + case AppStatus.DISABLED: + return true; + default: + return false; + } + } + + public isError(status: AppStatus): boolean { + return [AppStatus.ERROR_DISABLED, AppStatus.COMPILER_ERROR_DISABLED].includes(status); + } +} + +export const AppStatusUtils = new AppStatusUtilsDef(); diff --git a/packages/apps-engine/src/definition/IApp.ts b/packages/apps-engine/src/definition/IApp.ts new file mode 100644 index 000000000000..53faff9647f4 --- /dev/null +++ b/packages/apps-engine/src/definition/IApp.ts @@ -0,0 +1,90 @@ +import type { AppStatus } from './AppStatus'; +import type { IAppAccessors } from './accessors'; +import type { ILogger } from './accessors/ILogger'; +import type { IAppAuthorInfo } from './metadata/IAppAuthorInfo'; +import type { IAppInfo } from './metadata/IAppInfo'; + +export interface IApp { + /** + * Gets the status of this App. + * + * @return {AppStatus} the status/state of the App + */ + getStatus(): Promise; + + /** + * Get the name of this App. + * + * @return {string} the name + */ + getName(): string; + + /** + * Gets the sluggified name of this App. + * + * @return {string} the name slugged + */ + getNameSlug(): string; + + /** + * Gets the username of this App's app user. + * + * @return {string} the username of the app user + * + * @deprecated This method will be removed in the next major version. + * Please use read.getAppUser instead. + */ + getAppUserUsername(): string; + + /** + * Get the ID of this App, please see for how to obtain an ID for your App. + * + * @return {number} the ID + */ + getID(): string; + + /** + * Get the version of this App, using http://semver.org/. + * + * @return {string} the version + */ + getVersion(): string; + + /** + * Get the description of this App, mostly used to show to the clients/administrators. + * + * @return {string} the description + */ + getDescription(): string; + + /** + * Gets the API Version which this App depends on (http://semver.org/). + * This property is used for the dependency injections. + * + * @return {string} the required api version + */ + getRequiredApiVersion(): string; + + /** + * Gets the information regarding the author/maintainer of this App. + * + * @return author information + */ + getAuthorInfo(): IAppAuthorInfo; + + /** + * Gets the entirity of the App's information. + * + * @return App information + */ + getInfo(): IAppInfo; + + /** + * Gets the ILogger instance for this App. + * + * @return the logger instance + */ + getLogger(): ILogger; + + getAccessors(): IAppAccessors; +} diff --git a/packages/apps-engine/src/definition/LICENSE b/packages/apps-engine/src/definition/LICENSE new file mode 100644 index 000000000000..42ea81dc8cdb --- /dev/null +++ b/packages/apps-engine/src/definition/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2023 Rocket.Chat Technologies Corp. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/apps-engine/src/definition/accessors/IApiExtend.ts b/packages/apps-engine/src/definition/accessors/IApiExtend.ts new file mode 100644 index 000000000000..ab3210105e9c --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IApiExtend.ts @@ -0,0 +1,16 @@ +import type { IApi } from '../api'; + +/** + * This accessor provides methods for adding a custom api. + * It is provided during the initialization of your App + */ + +export interface IApiExtend { + /** + * Adds an api which can be called by external services lateron. + * Should an api already exists an error will be thrown. + * + * @param api the command information + */ + provideApi(api: IApi): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppAccessors.ts b/packages/apps-engine/src/definition/accessors/IAppAccessors.ts new file mode 100644 index 000000000000..c2ea3bfcacea --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppAccessors.ts @@ -0,0 +1,11 @@ +import type { IEnvironmentRead, IHttp, IRead } from '.'; +import type { IApiEndpointMetadata } from '../api'; +import type { IEnvironmentWrite } from './IEnvironmentWrite'; + +export interface IAppAccessors { + readonly environmentReader: IEnvironmentRead; + readonly environmentWriter: IEnvironmentWrite; + readonly reader: IRead; + readonly http: IHttp; + readonly providedApiEndpoints: Array; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts b/packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts new file mode 100644 index 000000000000..0ca1c08ba0dc --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppInstallationContext.ts @@ -0,0 +1,5 @@ +import type { IUser } from '../users'; + +export interface IAppInstallationContext { + user: IUser; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts b/packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts new file mode 100644 index 000000000000..96ddbfa03298 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppUninstallationContext.ts @@ -0,0 +1,5 @@ +import type { IUser } from '../users'; + +export interface IAppUninstallationContext { + user: IUser; +} diff --git a/packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts b/packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts new file mode 100644 index 000000000000..d0bcf7ea280b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IAppUpdateContext.ts @@ -0,0 +1,6 @@ +import type { IUser } from '../users'; + +export interface IAppUpdateContext { + user?: IUser; + oldAppVersion: string; +} diff --git a/packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts b/packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts new file mode 100644 index 000000000000..c78749fae59b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ICloudWorkspaceRead.ts @@ -0,0 +1,24 @@ +import type { IWorkspaceToken } from '../cloud/IWorkspaceToken'; + +/** + * Accessor that enables apps to read information + * related to the Cloud connectivity of the workspace. + * + * Methods in this accessor will usually connect to the + * Rocket.Chat Cloud, which means they won't work properly + * in air-gapped environment. + * + * This accessor available via `IRead` object, which is + * usually received as a parameter wherever it's available. + */ +export interface ICloudWorkspaceRead { + /** + * Returns an access token that can be used to access + * Cloud Services on the workspace's behalf. + * + * @param scope The scope that the token should be authorized with + * + * @RequiresPermission cloud.workspace-token; scopes: Array + */ + getWorkspaceToken(scope: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts b/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts new file mode 100644 index 000000000000..a58f127a0421 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IConfigurationExtend.ts @@ -0,0 +1,36 @@ +import type { IApiExtend } from './IApiExtend'; +import type { IExternalComponentsExtend } from './IExternalComponentsExtend'; +import type { IHttpExtend } from './IHttp'; +import type { ISchedulerExtend } from './ISchedulerExtend'; +import type { ISettingsExtend } from './ISettingsExtend'; +import type { ISlashCommandsExtend } from './ISlashCommandsExtend'; +import type { IUIExtend } from './IUIExtend'; +import type { IVideoConfProvidersExtend } from './IVideoConfProvidersExtend'; + +/** + * This accessor provides methods for declaring the configuration + * of your App. It is provided during initialization of your App. + */ +export interface IConfigurationExtend { + /** Accessor for customing the handling of IHttp requests and responses your App causes. */ + readonly http: IHttpExtend; + + /** Accessor for declaring the settings your App provides. */ + readonly settings: ISettingsExtend; + + /** Accessor for declaring the commands which your App provides. */ + readonly slashCommands: ISlashCommandsExtend; + + /** Accessor for declaring api endpoints. */ + readonly api: IApiExtend; + + readonly externalComponents: IExternalComponentsExtend; + + /** Accessor for declaring tasks that can be scheduled (like cron) */ + readonly scheduler: ISchedulerExtend; + /** Accessor for registering different elements in the host UI */ + readonly ui: IUIExtend; + + /** Accessor for declaring the videoconf providers which your App provides. */ + readonly videoConfProviders: IVideoConfProvidersExtend; +} diff --git a/packages/apps-engine/src/definition/accessors/IConfigurationModify.ts b/packages/apps-engine/src/definition/accessors/IConfigurationModify.ts new file mode 100644 index 000000000000..d0f818e2e028 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IConfigurationModify.ts @@ -0,0 +1,18 @@ +import type { ISchedulerModify } from './ISchedulerModify'; +import type { IServerSettingsModify } from './IServerSettingsModify'; +import type { ISlashCommandsModify } from './ISlashCommandsModify'; + +/** + * This accessor provides methods for modifying the configuration + * of Rocket.Chat. It is provided during "onEnable" of your App. + */ +export interface IConfigurationModify { + /** Accessor for modifying the settings inside of Rocket.Chat. */ + readonly serverSettings: IServerSettingsModify; + + /** Accessor for modifying the slash commands inside of Rocket.Chat. */ + readonly slashCommands: ISlashCommandsModify; + + /** Accessor for modifying schedulers */ + readonly scheduler: ISchedulerModify; +} diff --git a/packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts b/packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts new file mode 100644 index 000000000000..51dc0c2c4f92 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IDiscussionBuilder.ts @@ -0,0 +1,25 @@ +import type { IRoomBuilder } from '.'; +import type { IMessage } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; + +/** + * Interface for building out a room. + * Please note, a room creator, name, and type must be set otherwise you will NOT + * be able to successfully save the room object. + */ +export interface IDiscussionBuilder extends IRoomBuilder { + kind: RocketChatAssociationModel.DISCUSSION; + + setParentRoom(parentRoom: IRoom): IDiscussionBuilder; + + getParentRoom(): IRoom; + + setParentMessage(parentMessage: IMessage): IDiscussionBuilder; + + getParentMessage(): IMessage; + + setReply(reply: string): IDiscussionBuilder; + + getReply(): string; +} diff --git a/packages/apps-engine/src/definition/accessors/IEmailCreator.ts b/packages/apps-engine/src/definition/accessors/IEmailCreator.ts new file mode 100644 index 000000000000..d5d051bc2dff --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEmailCreator.ts @@ -0,0 +1,10 @@ +import type { IEmail } from '../email'; + +export interface IEmailCreator { + /** + * Sends an email through Rocket.Chat + * + * @param email the email data + */ + send(email: IEmail): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts b/packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts new file mode 100644 index 000000000000..81b50bee77c4 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEnvironmentRead.ts @@ -0,0 +1,27 @@ +import type { IEnvironmentalVariableRead } from './IEnvironmentalVariableRead'; +import type { IServerSettingRead } from './IServerSettingRead'; +import type { ISettingRead } from './ISettingRead'; + +/** + * Allows read-access to the App's settings, + * the certain server's settings along with environmental + * variables all of which are not user created. + */ +export interface IEnvironmentRead { + /** Gets an instance of the App's settings reader. */ + getSettings(): ISettingRead; + + /** + * Gets an instance of the Server's Settings reader. + * Please note: Due to security concerns, only a subset of settings + * are accessible. + */ + getServerSettings(): IServerSettingRead; + + /** + * Gets an instance of the Environmental Variables reader. + * Please note: Due to security concerns, only a subset of + * them are readable. + */ + getEnvironmentVariables(): IEnvironmentalVariableRead; +} diff --git a/packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts b/packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts new file mode 100644 index 000000000000..ab1869ab67cb --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEnvironmentWrite.ts @@ -0,0 +1,10 @@ +import type { IServerSettingUpdater } from './IServerSettingUpdater'; +import type { ISettingUpdater } from './ISettingUpdater'; + +/** + * Allows write-access to the App's settings, + */ +export interface IEnvironmentWrite { + getSettings(): ISettingUpdater; + getServerSettings(): IServerSettingUpdater; +} diff --git a/packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts b/packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts new file mode 100644 index 000000000000..3bb77b033e83 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IEnvironmentalVariableRead.ts @@ -0,0 +1,11 @@ +/** A reader for reading the Environmental Variables. */ +export interface IEnvironmentalVariableRead { + /** Gets the value for a variable. */ + getValueByName(envVarName: string): Promise; + + /** Checks to see if Apps can access the given variable name. */ + isReadable(envVarName: string): Promise; + + /** Checks to see if any value is set for the given variable name. */ + isSet(envVarName: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts b/packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts new file mode 100644 index 000000000000..6a3dc781056f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IExternalComponentsExtend.ts @@ -0,0 +1,17 @@ +import type { IExternalComponent } from '../externalComponent'; + +/** + * This accessor provides a method for registering external + * components. This is provided during the initialization of your App. + */ +export interface IExternalComponentsExtend { + /** + * Register an external component to the system. + * If you call this method twice and the component + * has the same name as before, the first one will be + * overwritten as the names provided **must** be unique. + * + * @param externalComponent the external component to be registered + */ + register(externalComponent: IExternalComponent): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IHttp.ts b/packages/apps-engine/src/definition/accessors/IHttp.ts new file mode 100644 index 000000000000..8c5eeb9a4d55 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IHttp.ts @@ -0,0 +1,202 @@ +import type { IPersistence } from './IPersistence'; +import type { IRead } from './IRead'; + +/** + * The Http package allows users to call out to an external web service. + * Based off of: https://github.com/meteor-typings/meteor/blob/master/1.4/main.d.ts#L869 + */ +export interface IHttp { + get(url: string, options?: IHttpRequest): Promise; + + post(url: string, options?: IHttpRequest): Promise; + + put(url: string, options?: IHttpRequest): Promise; + + del(url: string, options?: IHttpRequest): Promise; + + patch(url: string, options?: IHttpRequest): Promise; +} + +export enum RequestMethod { + GET = 'get', + POST = 'post', + PUT = 'put', + DELETE = 'delete', + HEAD = 'head', + OPTIONS = 'options', + PATCH = 'patch', +} + +export interface IHttpRequest { + content?: string; + data?: any; + query?: string; + params?: { + [key: string]: string; + }; + auth?: string; + headers?: { + [key: string]: string; + }; + timeout?: number; + /** + * The encoding to be used on response data. + * + * If null, the body is returned as a Buffer. Anything else (including the default value of undefined) + * will be passed as the encoding parameter to toString() (meaning this is effectively 'utf8' by default). + * (Note: if you expect binary data, you should set encoding: null.) + */ + encoding?: string | null; + /** + * if `true`, requires SSL certificates be valid. + * + * Defaul: `true`; + */ + strictSSL?: boolean; + /** + * If `true`, the server certificate is verified against the list of supplied CAs. + * + * Default: `true`. + * + * https://nodejs.org/api/tls.html#tls_tls_connect_options_callback + */ + rejectUnauthorized?: boolean; +} + +export interface IHttpResponse { + url: string; + method: RequestMethod; + statusCode: number; + headers?: { + [key: string]: string; + }; + content?: string; + data?: any; +} + +export interface IHttpExtend { + /** + * A method for providing a single header which is added to every request. + * + * @param key the name of the header + * @param value the header's content + */ + provideDefaultHeader(key: string, value: string): void; + + /** + * A method for providing more than one header which are added to every request. + * + * @param headers an object with strings as the keys (header name) and strings as values (header content) + */ + provideDefaultHeaders(headers: { [key: string]: string }): void; + + /** + * A method for providing a single query parameter which is added to every request. + * + * @param key the name of the query parameter + * @param value the query parameter's content + */ + provideDefaultParam(key: string, value: string): void; + + /** + * A method for providing more than one query parameters which are added to every request. + * + * @param headers an object with strings as the keys (parameter name) and strings as values (parameter content) + */ + provideDefaultParams(params: { [key: string]: string }): void; + + /** + * Method for providing a function which is called before every request is called out to the final destination. + * This can be called more than once which means there can be more than one handler. The order provided is the order called. + * Note: if this handler throws an error when it is executed then the request will be aborted. + * + * @param handler the instance of the IHttpPreRequestHandler + */ + providePreRequestHandler(handler: IHttpPreRequestHandler): void; + + /** + * Method for providing a function which is called after every response is got from the url and before the result is returned. + * This can be called more than once which means there can be more than one handler. The order provided is the order called. + * Note: if this handler throws an error when it is executed then the respone will not be returned + * + * @param handler the instance of the IHttpPreResponseHandler + */ + providePreResponseHandler(handler: IHttpPreResponseHandler): void; + + /** + * A method for getting all of the default headers provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getDefaultHeaders(): Map; + + /** + * A method for getting all of the default parameters provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getDefaultParams(): Map; + + /** + * A method for getting all of the pre-request handlers provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getPreRequestHandlers(): Array; + + /** + * A method for getting all of the pre-response handlers provided, the value is a readonly and any modifications done will be ignored. + * Please use the provider methods for adding them. + */ + getPreResponseHandlers(): Array; +} + +export interface IHttpPreRequestHandler { + executePreHttpRequest(url: string, request: IHttpRequest, read: IRead, persistence: IPersistence): Promise; +} + +export interface IHttpPreResponseHandler { + executePreHttpResponse(response: IHttpResponse, read: IRead, persistence: IPersistence): Promise; +} + +export enum HttpStatusCode { + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + TEMPORARY_REDIRECT = 307, + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + REQUEST_ENTITY_TOO_LARGE = 413, + REQUEST_URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + REQUESTED_RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + UNPROCESSABLE_ENTITY = 422, + TOO_MANY_REQUESTS = 429, + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatCreator.ts b/packages/apps-engine/src/definition/accessors/ILivechatCreator.ts new file mode 100644 index 000000000000..56a3ec17ec27 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatCreator.ts @@ -0,0 +1,43 @@ +import type { ILivechatRoom, IVisitor } from '../livechat'; +import type { IUser } from '../users'; + +export interface IExtraRoomParams { + source?: ILivechatRoom['source']; + customFields?: { + [key: string]: unknown; + }; +} + +export interface ILivechatCreator { + /** + * Creates a room to connect the `visitor` to an `agent`. + * + * This method uses the Livechat routing method configured + * in the server + * + * @param visitor The Livechat Visitor that started the conversation + * @param agent The agent responsible for the room + */ + createRoom(visitor: IVisitor, agent: IUser, extraParams?: IExtraRoomParams): Promise; + + /** + * @deprecated Use `createAndReturnVisitor` instead. + * Creates a Livechat visitor + * + * @param visitor Data of the visitor to be created + */ + createVisitor(visitor: IVisitor): Promise; + + /** + * Creates a Livechat visitor + * + * @param visitor Data of the visitor to be created + */ + createAndReturnVisitor(visitor: IVisitor): Promise; + + /** + * Creates a token to be used when + * creating a new livechat visitor + */ + createToken(): string; +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts b/packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts new file mode 100644 index 000000000000..c36b078f53a6 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatMessageBuilder.ts @@ -0,0 +1,219 @@ +import type { ILivechatMessage, IVisitor } from '../livechat'; +import type { IMessageAttachment } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; +import type { IMessageBuilder } from './IMessageBuilder'; + +/** + * Interface for building out a livechat message. + * Please note, that a room and sender must be associated otherwise you will NOT + * be able to successfully save the message object. + */ +export interface ILivechatMessageBuilder { + kind: RocketChatAssociationModel.LIVECHAT_MESSAGE; + + /** + * Provides a convient way to set the data for the message. + * Note: Providing an "id" field here will be ignored. + * + * @param message the message data to set + */ + setData(message: ILivechatMessage): ILivechatMessageBuilder; + + /** + * Sets the room where this message should be sent to. + * + * @param room the room where to send + */ + setRoom(room: IRoom): ILivechatMessageBuilder; + + /** + * Gets the room where this message was sent to. + */ + getRoom(): IRoom; + + /** + * Sets the sender of this message. + * + * @param sender the user sending the message + */ + setSender(sender: IUser): ILivechatMessageBuilder; + + /** + * Gets the User which sent the message. + */ + getSender(): IUser; + + /** + * Sets the text of the message. + * + * @param text the actual text + */ + setText(text: string): ILivechatMessageBuilder; + + /** + * Gets the message text. + */ + getText(): string; + + /** + * Sets the emoji to use for the avatar, this overwrites the current avatar + * whether it be the user's or the avatar url provided. + * + * @param emoji the emoji code + */ + setEmojiAvatar(emoji: string): ILivechatMessageBuilder; + + /** + * Gets the emoji used for the avatar. + */ + getEmojiAvatar(): string; + + /** + * Sets the url which to display for the avatar, this overwrites the current + * avatar whether it be the user's or an emoji one. + * + * @param avatarUrl image url to use as the avatar + */ + setAvatarUrl(avatarUrl: string): ILivechatMessageBuilder; + + /** + * Gets the url used for the avatar. + */ + getAvatarUrl(): string; + + /** + * Sets the display text of the sender's username that is visible. + * + * @param alias the username alias to display + */ + setUsernameAlias(alias: string): ILivechatMessageBuilder; + + /** + * Gets the display text of the sender's username that is visible. + */ + getUsernameAlias(): string; + + /** + * Adds one attachment to the message's list of attachments, this will not + * overwrite any existing ones but just adds. + * + * @param attachment the attachment to add + */ + addAttachment(attachment: IMessageAttachment): ILivechatMessageBuilder; + + /** + * Sets the attachments for the message, replacing and destroying all of the current attachments. + * + * @param attachments array of the attachments + */ + setAttachments(attachments: Array): ILivechatMessageBuilder; + + /** + * Gets the attachments array for the message + */ + getAttachments(): Array; + + /** + * Replaces an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to replace + * @param attachment the attachment to replace with + */ + replaceAttachment(position: number, attachment: IMessageAttachment): ILivechatMessageBuilder; + + /** + * Removes an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to remove + */ + removeAttachment(position: number): ILivechatMessageBuilder; + + /** + * Sets the user who is editing this message. + * This is required if you are modifying an existing message. + * + * @param user the editor + */ + setEditor(user: IUser): ILivechatMessageBuilder; + + /** + * Gets the user who edited the message + */ + getEditor(): IUser; + + /** + * Sets whether this message can group with others. + * This is desirable if you want to avoid confusion with other integrations. + * + * @param groupable whether this message can group with others + */ + setGroupable(groupable: boolean): ILivechatMessageBuilder; + + /** + * Gets whether this message can group with others. + */ + getGroupable(): boolean; + + /** + * Sets whether this message should have any URLs in the text + * parsed by Rocket.Chat and get the details added to the message's + * attachments. + * + * @param parseUrls whether URLs should be parsed in this message + */ + setParseUrls(parseUrls: boolean): ILivechatMessageBuilder; + + /** + * Gets whether this message should have its URLs parsed + */ + getParseUrls(): boolean; + + /** + * Set the token of the livechat visitor that + * sent the message + * + * @param token The Livechat visitor's token + */ + setToken(token: string): ILivechatMessageBuilder; + + /** + * Gets the token of the livechat visitor that + * sent the message + */ + getToken(): string; + + /** + * If the sender of the message is a Livechat Visitor, + * set the visitor who sent the message. + * + * If you set the visitor property of a message, the + * sender will be emptied + * + * @param visitor The visitor who sent the message + */ + setVisitor(visitor: IVisitor): ILivechatMessageBuilder; + + /** + * Get the visitor who sent the message, + * if any + */ + getVisitor(): IVisitor; + + /** + * Gets the resulting message that has been built up to the point of calling it. + * + * *Note:* This will error out if the Room has not been defined OR if the room + * is not of type RoomType.LIVE_CHAT. + */ + getMessage(): ILivechatMessage; + + /** + * Returns a message builder based on the + * livechat message of this builder + */ + getMessageBuilder(): IMessageBuilder; +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatRead.ts b/packages/apps-engine/src/definition/accessors/ILivechatRead.ts new file mode 100644 index 000000000000..a756d162c337 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatRead.ts @@ -0,0 +1,38 @@ +import type { IDepartment } from '../livechat'; +import type { ILivechatRoom } from '../livechat/ILivechatRoom'; +import type { IVisitor } from '../livechat/IVisitor'; +import type { IMessage } from '../messages'; + +export interface ILivechatRead { + /** + * Gets online status of the livechat. + * @param departmentId (optional) the id of the livechat department + * @deprecated use `isOnlineAsync` instead + */ + isOnline(departmentId?: string): boolean; + /** + * Gets online status of the livechat. + * @param departmentId (optional) the id of the livechat department + */ + isOnlineAsync(departmentId?: string): Promise; + getDepartmentsEnabledWithAgents(): Promise>; + getLivechatRooms(visitor: IVisitor, departmentId?: string): Promise>; + getLivechatOpenRoomsByAgentId(agentId: string): Promise>; + getLivechatTotalOpenRoomsByAgentId(agentId: string): Promise; + /** + * @deprecated This method does not adhere to the conversion practices applied + * elsewhere in the Apps-Engine and will be removed in the next major version. + * Prefer the alternative methods to fetch visitors. + */ + getLivechatVisitors(query: object): Promise>; + getLivechatVisitorById(id: string): Promise; + getLivechatVisitorByEmail(email: string): Promise; + getLivechatVisitorByToken(token: string): Promise; + getLivechatVisitorByPhoneNumber(phoneNumber: string): Promise; + getLivechatDepartmentByIdOrName(value: string): Promise; + /** + * @experimental we do not encourage the wider usage of this method, + * as we're evaluating its performance and fit for the API. + */ + _fetchLivechatRoomMessages(roomId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts b/packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts new file mode 100644 index 000000000000..fb75cf9ecf3b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILivechatUpdater.ts @@ -0,0 +1,33 @@ +import type { ILivechatTransferData, IVisitor } from '../livechat'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; + +export interface ILivechatUpdater { + /** + * Transfer a Livechat visitor to another room + * + * @param visitor Visitor to be transferred + * @param transferData The data to execute the transferring + */ + transferVisitor(visitor: IVisitor, transferData: ILivechatTransferData): Promise; + + /** + * Closes a Livechat room + * + * @param room The room to be closed + * @param comment The comment explaining the reason for closing the room + * @param closer The user that closes the room + */ + closeRoom(room: IRoom, comment: string, closer?: IUser): Promise; + + /** + * Set a livechat visitor's custom fields by its token + * @param token The visitor's token + * @param key The key in the custom fields + * @param value The value to be set + * @param overwrite Whether overwrite or not + * + * @returns Promise to whether success or not + */ + setCustomFields(token: IVisitor['token'], key: string, value: string, overwrite: boolean): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ILogEntry.ts b/packages/apps-engine/src/definition/accessors/ILogEntry.ts new file mode 100644 index 000000000000..4dc46693b6d9 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILogEntry.ts @@ -0,0 +1,22 @@ +export enum LogMessageSeverity { + DEBUG = 'debug', + INFORMATION = 'info', + LOG = 'log', + WARNING = 'warning', + ERROR = 'error', + SUCCESS = 'success', +} + +/** + * Message which will be passed to a UI (either in a log or in the application's UI) + */ +export interface ILogEntry { + /** The function name who did this logging, this is automatically added (can be null). */ + caller?: string; + /** The severity rate, this is automatically added. */ + severity: LogMessageSeverity; + /** When this entry was made. */ + timestamp: Date; + /** The items which were logged. */ + args: Array; +} diff --git a/packages/apps-engine/src/definition/accessors/ILogger.ts b/packages/apps-engine/src/definition/accessors/ILogger.ts new file mode 100644 index 000000000000..eac6e531802d --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ILogger.ts @@ -0,0 +1,29 @@ +import type { AppMethod } from '../metadata/AppMethod'; +import type { ILogEntry } from './ILogEntry'; + +/** + * This logger provides a way to log various levels to the entire system. + * When used, the items passed in will be logged to the database. This will + * allow people to easily see what happened (users) or debug what went wrong. + */ +export interface ILogger { + method: `${AppMethod}`; + + debug(...items: Array): void; + info(...items: Array): void; + log(...items: Array): void; + warn(...items: Array): void; + error(...items: Array): void; + success(...items: Array): void; + + /** Gets the entries logged. */ + getEntries(): Array; + /** Gets the method which this logger is for. */ + getMethod(): `${AppMethod}`; + /** Gets when this logger was constructed. */ + getStartTime(): Date; + /** Gets the end time, usually Date.now(). */ + getEndTime(): Date; + /** Gets the amount of time this was a logger, start - Date.now(). */ + getTotalTime(): number; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageBuilder.ts b/packages/apps-engine/src/definition/accessors/IMessageBuilder.ts new file mode 100644 index 000000000000..024a4b123ff8 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageBuilder.ts @@ -0,0 +1,236 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IMessage, IMessageAttachment } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; +import type { BlockBuilder, IBlock } from '../uikit'; +import type { IUser } from '../users'; + +/** + * Interface for building out a message. + * Please note, that a room and sender must be associated otherwise you will NOT + * be able to successfully save the message object. + */ +export interface IMessageBuilder { + kind: RocketChatAssociationModel.MESSAGE; + + /** + * Provides a convenient way to set the data for the message. + * Note: Providing an "id" field here will be ignored. + * + * @param message the message data to set + */ + setData(message: IMessage): IMessageBuilder; + + /** + * Provides a convenient way to set the data for the message + * keeping the "id" field so as to update the message later. + * + * @param message the message data to set + * @param editor the user who edited the updated message + */ + setUpdateData(message: IMessage, editor: IUser): IMessageBuilder; + + /** + * Sets the thread to which this message belongs, if any. + * + * @param threadId The id of the thread + */ + setThreadId(threadId: string): IMessageBuilder; + + /** + * Retrieves the threadId to which this message belongs, + * if any. + * + * If you would like to retrieve the actual message that + * the thread originated from, you can use the + * `IMessageRead.getById()` method + */ + getThreadId(): string; + + /** + * Sets the room where this message should be sent to. + * + * @param room the room where to send + */ + setRoom(room: IRoom): IMessageBuilder; + + /** + * Gets the room where this message was sent to. + */ + getRoom(): IRoom; + + /** + * Sets the sender of this message. + * + * @param sender the user sending the message + */ + setSender(sender: IUser): IMessageBuilder; + + /** + * Gets the User which sent the message. + */ + getSender(): IUser; + + /** + * Sets the text of the message. + * + * @param text the actual text + */ + setText(text: string): IMessageBuilder; + + /** + * Gets the message text. + */ + getText(): string; + + /** + * Sets the emoji to use for the avatar, this overwrites the current avatar + * whether it be the user's or the avatar url provided. + * + * @param emoji the emoji code + */ + setEmojiAvatar(emoji: string): IMessageBuilder; + + /** + * Gets the emoji used for the avatar. + */ + getEmojiAvatar(): string; + + /** + * Sets the url which to display for the avatar, this overwrites the current + * avatar whether it be the user's or an emoji one. + * + * @param avatarUrl image url to use as the avatar + */ + setAvatarUrl(avatarUrl: string): IMessageBuilder; + + /** + * Gets the url used for the avatar. + */ + getAvatarUrl(): string; + + /** + * Sets the display text of the sender's username that is visible. + * + * @param alias the username alias to display + */ + setUsernameAlias(alias: string): IMessageBuilder; + + /** + * Gets the display text of the sender's username that is visible. + */ + getUsernameAlias(): string; + + /** + * Adds one attachment to the message's list of attachments, this will not + * overwrite any existing ones but just adds. + * + * @param attachment the attachment to add + */ + addAttachment(attachment: IMessageAttachment): IMessageBuilder; + + /** + * Sets the attachments for the message, replacing and destroying all of the current attachments. + * + * @param attachments array of the attachments + */ + setAttachments(attachments: Array): IMessageBuilder; + + /** + * Gets the attachments array for the message + */ + getAttachments(): Array; + + /** + * Replaces an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to replace + * @param attachment the attachment to replace with + */ + replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder; + + /** + * Removes an attachment at the given position (index). + * If there is no attachment at that position, there will be an error thrown. + * + * @param position the index of the attachment to remove + */ + removeAttachment(position: number): IMessageBuilder; + + /** + * Sets the user who is editing this message. + * This is required if you are modifying an existing message. + * + * @param user the editor + */ + setEditor(user: IUser): IMessageBuilder; + + /** + * Gets the user who edited the message + */ + getEditor(): IUser; + + /** + * Sets whether this message can group with others. + * This is desirable if you want to avoid confusion with other integrations. + * + * @param groupable whether this message can group with others + */ + setGroupable(groupable: boolean): IMessageBuilder; + + /** + * Gets whether this message can group with others. + */ + getGroupable(): boolean; + + /** + * Sets whether this message should have any URLs in the text + * parsed by Rocket.Chat and get the details added to the message's + * attachments. + * + * @param parseUrls whether URLs should be parsed in this message + */ + setParseUrls(parseUrls: boolean): IMessageBuilder; + + /** + * Gets whether this message should have its URLs parsed + */ + getParseUrls(): boolean; + + /** + * Gets the resulting message that has been built up to the point of calling it. + * + * *Note:* This will error out if the Room has not been defined. + */ + getMessage(): IMessage; + + /** + * Adds a block collection to the message's + * own collection + */ + addBlocks(blocks: BlockBuilder | Array): IMessageBuilder; + + /** + * Sets the block collection of the message + * + * @param blocks + */ + setBlocks(blocks: BlockBuilder | Array): IMessageBuilder; + + /** + * Gets the block collection of the message + */ + getBlocks(): Array; + + /** + * Adds a custom field to the message. + * Note: This key can not already exist or it will throw an error. + * Note: The key must not contain a period in it, an error will be thrown. + * + * @param key the name of the custom field + * @param value the value of this custom field + */ + addCustomField(key: string, value: any): IMessageBuilder; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageExtender.ts b/packages/apps-engine/src/definition/accessors/IMessageExtender.ts new file mode 100644 index 000000000000..7db010bae081 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageExtender.ts @@ -0,0 +1,36 @@ +import type { IMessage, IMessageAttachment } from '../messages'; +import type { RocketChatAssociationModel } from '../metadata'; + +export interface IMessageExtender { + kind: RocketChatAssociationModel.MESSAGE; + + /** + * Adds a custom field to the message. + * Note: This key can not already exist or it will throw an error. + * Note: The key must not contain a period in it, an error will be thrown. + * + * @param key the name of the custom field + * @param value the value of this custom field + */ + addCustomField(key: string, value: any): IMessageExtender; + + /** + * Adds a single attachment to the message. + * + * @param attachment the item to add + */ + addAttachment(attachment: IMessageAttachment): IMessageExtender; + + /** + * Adds all of the provided attachments to the message. + * + * @param attachments an array of attachments + */ + addAttachments(attachments: Array): IMessageExtender; + + /** + * Gets the resulting message that has been extended at the point of calling it. + * Note: modifying the returned value will have no effect. + */ + getMessage(): IMessage; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageRead.ts b/packages/apps-engine/src/definition/accessors/IMessageRead.ts new file mode 100644 index 000000000000..10c99d26388b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageRead.ts @@ -0,0 +1,15 @@ +import type { IMessage } from '../messages/index'; +import type { IRoom } from '../rooms/IRoom'; +import type { IUser } from '../users/IUser'; + +/** + * This accessor provides methods for accessing + * messages in a read-only-fashion. + */ +export interface IMessageRead { + getById(id: string): Promise; + + getSenderUser(messageId: string): Promise; + + getRoom(messageId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IMessageUpdater.ts b/packages/apps-engine/src/definition/accessors/IMessageUpdater.ts new file mode 100644 index 000000000000..b21baacae04f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IMessageUpdater.ts @@ -0,0 +1,21 @@ +import type { Reaction } from '../messages'; + +export interface IMessageUpdater { + /** + * Add a reaction to a message + * + * @param messageId the id of the message + * @param userId the id of the user + * @param reaction the reaction + */ + addReaction(messageId: string, userId: string, reaction: Reaction): Promise; + + /** + * Remove a reaction from a message + * + * @param messageId the id of the message + * @param userId the id of the user + * @param reaction the reaction + */ + removeReaction(messageId: string, userId: string, reaction: Reaction): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModerationModify.ts b/packages/apps-engine/src/definition/accessors/IModerationModify.ts new file mode 100644 index 000000000000..6b0f54968a04 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModerationModify.ts @@ -0,0 +1,27 @@ +import type { IMessage } from '../messages'; +import type { IUser } from '../users'; + +export interface IModerationModify { + /** + * Provides a way for Apps to report a message. + * @param messageId the messageId to report + * @param description the description of the report + * @param userId the userId to be reported + * @param appId the app id + */ + report(messageId: string, description: string, userId: string, appId: string): Promise; + + /** + * Provides a way for Apps to dismiss reports by message id. + * @param messageId the messageId to dismiss reports + * @param appId the app id + */ + dismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise; + + /** + * Provides a way for Apps to dismiss reports by user id. + * @param userId the userId to dismiss reports + * @param appId the app id + */ + dismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModify.ts b/packages/apps-engine/src/definition/accessors/IModify.ts new file mode 100644 index 000000000000..e76e4cc76824 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModify.ts @@ -0,0 +1,45 @@ +import type { IModerationModify } from './IModerationModify'; +import type { IModifyCreator } from './IModifyCreator'; +import type { IModifyDeleter } from './IModifyDeleter'; +import type { IModifyExtender } from './IModifyExtender'; +import type { IModifyUpdater } from './IModifyUpdater'; +import type { INotifier } from './INotifier'; +import type { IOAuthAppsModify } from './IOAuthAppsModify'; +import type { ISchedulerModify } from './ISchedulerModify'; +import type { IUIController } from './IUIController'; + +export interface IModify { + getCreator(): IModifyCreator; + + getDeleter(): IModifyDeleter; + + getExtender(): IModifyExtender; + + getUpdater(): IModifyUpdater; + + /** + * Gets the accessor for sending notifications to a user or users in a room. + * + * @returns the notifier accessor + */ + getNotifier(): INotifier; + /** + * Gets the accessor for interacting with the UI + */ + getUiController(): IUIController; + + /** + * Gets the accessor for creating scheduled jobs + */ + getScheduler(): ISchedulerModify; + + /** + * Gets the accessor for creating OAuth apps + */ + getOAuthAppsModifier(): IOAuthAppsModify; + /** + * Gets the accessor for modifying moderation + * @returns the moderation accessor + */ + getModerationModifier(): IModerationModify; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyCreator.ts b/packages/apps-engine/src/definition/accessors/IModifyCreator.ts new file mode 100644 index 000000000000..6c2acd50493c --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyCreator.ts @@ -0,0 +1,100 @@ +import type { ILivechatMessage } from '../livechat'; +import type { IMessage } from '../messages'; +import type { IRoom } from '../rooms'; +import type { BlockBuilder } from '../uikit'; +import type { IBotUser } from '../users/IBotUser'; +import type { AppVideoConference } from '../videoConferences'; +import type { IDiscussionBuilder } from './IDiscussionBuilder'; +import type { IEmailCreator } from './IEmailCreator'; +import type { ILivechatCreator } from './ILivechatCreator'; +import type { ILivechatMessageBuilder } from './ILivechatMessageBuilder'; +import type { IMessageBuilder } from './IMessageBuilder'; +import type { IRoomBuilder } from './IRoomBuilder'; +import type { IUploadCreator } from './IUploadCreator'; +import type { IUserBuilder } from './IUserBuilder'; +import type { IVideoConferenceBuilder } from './IVideoConferenceBuilder'; + +export interface IModifyCreator { + /** + * Get the creator object responsible for the + * Livechat integrations + */ + getLivechatCreator(): ILivechatCreator; + + /** + * Get the creator object responsible for the upload. + */ + getUploadCreator(): IUploadCreator; + + /** + * Gets the creator object responsible for email sending + */ + getEmailCreator(): IEmailCreator; + + /** + * @deprecated please prefer the rocket.chat/ui-kit components + * + * Gets a new instance of a BlockBuilder + */ + getBlockBuilder(): BlockBuilder; + /** + * Starts the process for building a new message object. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IMessageBuilder instance + */ + startMessage(data?: IMessage): IMessageBuilder; + + /** + * Starts the process for building a new livechat message object. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IMessageBuilder instance + */ + startLivechatMessage(data?: ILivechatMessage): ILivechatMessageBuilder; + + /** + * Starts the process for building a new room. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IRoomBuilder instance + */ + startRoom(data?: IRoom): IRoomBuilder; + + /** + * Starts the process for building a new discussion. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IDiscussionBuilder instance + */ + startDiscussion(data?: Partial): IDiscussionBuilder; + + /** + * Starts the process for building a new video conference. + * + * @param data (optional) the initial data to pass into the builder, + * @return an IVideoConferenceBuilder instance + */ + startVideoConference(data?: Partial): IVideoConferenceBuilder; + + /** + * Starts the process for building a new bot user. + * + * @param data (optional) the initial data to pass into the builder, + * the `id` property will be ignored + * @return an IUserBuilder instance + */ + startBotUser(data?: Partial): IUserBuilder; + + /** + * Finishes the creating process, saving the object to the database. + * + * @param builder the builder instance + * @return the resulting `id` of the resulting object + */ + finish(builder: IMessageBuilder | ILivechatMessageBuilder | IRoomBuilder | IDiscussionBuilder | IVideoConferenceBuilder | IUserBuilder): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyDeleter.ts b/packages/apps-engine/src/definition/accessors/IModifyDeleter.ts new file mode 100644 index 000000000000..7d1103ba13f3 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyDeleter.ts @@ -0,0 +1,12 @@ +import type { IMessage } from '../messages'; +import type { IUser, UserType } from '../users'; + +export interface IModifyDeleter { + deleteRoom(roomId: string): Promise; + + deleteUsers(appId: Exclude, userType: UserType.APP | UserType.BOT): Promise; + + deleteMessage(message: IMessage, user: IUser): Promise; + + removeUsersFromRoom(roomId: string, usernames: Array): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyExtender.ts b/packages/apps-engine/src/definition/accessors/IModifyExtender.ts new file mode 100644 index 000000000000..786b23975407 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyExtender.ts @@ -0,0 +1,40 @@ +import type { IUser } from '../users'; +import type { IMessageExtender } from './IMessageExtender'; +import type { IRoomExtender } from './IRoomExtender'; +import type { IVideoConferenceExtender } from './IVideoConferenceExtend'; + +export interface IModifyExtender { + /** + * Modifies a message in a non-destructive way: Properties can be added to it, + * but existing properties cannot be changed. + * + * @param messageId the id of the message to be extended + * @param updater the user who is updating/extending the message + * @return the extender instance for the message + */ + extendMessage(messageId: string, updater: IUser): Promise; + + /** + * Modifies a room in a non-destructive way: Properties can be added to it, + * but existing properties cannot be changed. + * + * @param roomId the id of the room to be extended + * @param updater the user who is updating/extending the room + * @return the extender instance for the room + */ + extendRoom(roomId: string, updater: IUser): Promise; + + /** + * Modifies a video conference in a non-destructive way: Properties can be added to it, + * but existing properties cannot be changed. + */ + extendVideoConference(id: string): Promise; + + /** + * Finishes the extending process, saving the object to the database. + * Note: If there is an issue or error while updating, this will throw an error. + * + * @param extender the extender instance + */ + finish(extender: IRoomExtender | IMessageExtender | IVideoConferenceExtender): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IModifyUpdater.ts b/packages/apps-engine/src/definition/accessors/IModifyUpdater.ts new file mode 100644 index 000000000000..60dcf90b2df7 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IModifyUpdater.ts @@ -0,0 +1,52 @@ +import type { IUser } from '../users'; +import type { ILivechatUpdater } from './ILivechatUpdater'; +import type { IMessageBuilder } from './IMessageBuilder'; +import type { IMessageUpdater } from './IMessageUpdater'; +import type { IRoomBuilder } from './IRoomBuilder'; +import type { IUserUpdater } from './IUserUpdater'; + +export interface IModifyUpdater { + /** + * Get the updater object responsible for the + * Livechat integrations + */ + getLivechatUpdater(): ILivechatUpdater; + + /** + * Gets the update object responsible for + * methods that update users + */ + getUserUpdater(): IUserUpdater; + + /** + * Get the updater object responsible for + * methods that update messages + */ + getMessageUpdater(): IMessageUpdater; + + /** + * Modifies an existing message. + * Raises an exception if a non-existent messageId is supplied + * + * @param messageId the id of the existing message to modfiy and build + * @param updater the user who is updating the message + */ + message(messageId: string, updater: IUser): Promise; + + /** + * Modifies an existing room. + * Raises an exception if a non-existent roomId is supplied + * + * @param roomId the id of the existing room to modify and build + * @param updater the user who is updating the room + */ + room(roomId: string, updater: IUser): Promise; + + /** + * Finishes the updating process, saving the object to the database. + * Note: If there is an issue or error while updating, this will throw an error. + * + * @param builder the builder instance + */ + finish(builder: IMessageBuilder | IRoomBuilder): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/INotifier.ts b/packages/apps-engine/src/definition/accessors/INotifier.ts new file mode 100644 index 000000000000..a41fb22c4ff6 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/INotifier.ts @@ -0,0 +1,63 @@ +import type { IMessage } from '../messages'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; +import type { IMessageBuilder } from './IMessageBuilder'; + +export enum TypingScope { + Room = 'room', +} + +export interface ITypingOptions { + /** + * The typing scope where the typing message should be presented, + * TypingScope.Room by default. + */ + scope?: TypingScope; + /** + * The id of the typing scope + * + * TypingScope.Room <-> room.id + */ + id: string; + /** + * The name of the user who is typing the message + * + * **Note**: If not provided, it will use app assigned + * user's name by default. + */ + username?: string; +} + +export interface INotifier { + /** + * Notifies the provided user of the provided message. + * + * **Note**: Notifications only are shown to the user if they are + * online and it only stays around for the duration of their session. + * + * @param user The user who should be notified + * @param message The message with the content to notify the user about + */ + notifyUser(user: IUser, message: IMessage): Promise; + + /** + * Notifies all of the users in the provided room. + * + * **Note**: Notifications only are shown to those online + * and it only stays around for the duration of their session. + * + * @param room The room which to notify the users in + * @param message The message content to notify users about + */ + notifyRoom(room: IRoom, message: IMessage): Promise; + + /** + * Notifies all of the users a typing indicator in the provided scope. + * + * @returns a cancellation function to stop typing + */ + typing(options: ITypingOptions): Promise<() => Promise>; + + /** Gets a new message builder for building a notification message. */ + getMessageBuilder(): IMessageBuilder; +} diff --git a/packages/apps-engine/src/definition/accessors/IOAuthApp.ts b/packages/apps-engine/src/definition/accessors/IOAuthApp.ts new file mode 100644 index 000000000000..1c2edbe19c95 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOAuthApp.ts @@ -0,0 +1,13 @@ +export interface IOAuthApp { + id: string; + name: string; + active: boolean; + clientId?: string; + clientSecret?: string; + redirectUri: string; + createdAt?: string; + updatedAt?: string; + createdBy: { username: string; id: string }; +} + +export type IOAuthAppParams = Omit; diff --git a/packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts b/packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts new file mode 100644 index 000000000000..72d0a4ddce5a --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOAuthAppsModify.ts @@ -0,0 +1,23 @@ +import type { IOAuthAppParams } from './IOAuthApp'; + +export interface IOAuthAppsModify { + /** + * Create an OAuthApp + * @param OAuthApp - the OAuth app to create, in case the clientId and the clientSecret is not sent it will generate automatically + * @param appId - the app id + */ + createOAuthApp(OAuthApp: IOAuthAppParams, appId: string): Promise; + /** + * Update the OAuth app info + * @param OAuthApp - OAuth data that will be updated + * @param id - OAuth app id + * @param appId - the app id + */ + updateOAuthApp(OAuthApp: IOAuthAppParams, id: string, appId: string): Promise; + /** + * Deletes the OAuth app + * @param id - OAuth app id + * @param appId - the app id + */ + deleteOAuthApp(id: string, appId: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts b/packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts new file mode 100644 index 000000000000..97d544cd9366 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IOAuthAppsReader.ts @@ -0,0 +1,16 @@ +import type { IOAuthApp } from './IOAuthApp'; + +export interface IOAuthAppsReader { + /** + * Returns the OAuth app info by its id + * @param id - OAuth app id + * @param appId - the app id + */ + getOAuthAppById(id: string, appId: string): Promise; + /** + * Returns the OAuth app info by its name + * @param name - OAuth app name + * @param appId - the app id + */ + getOAuthAppByName(name: string, appId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IPersistence.ts b/packages/apps-engine/src/definition/accessors/IPersistence.ts new file mode 100644 index 000000000000..30f1d676539e --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IPersistence.ts @@ -0,0 +1,97 @@ +import type { RocketChatAssociationRecord } from '../metadata'; + +/** + * Provides an accessor write data to the App's persistent storage. + * A App only has access to its own persistent storage and does not + * have access to any other App's. + */ +export interface IPersistence { + /** + * Creates a new record in the App's persistent storage, returning the resulting "id". + * + * @param data the actual data to store, must be an object otherwise it will error out. + * @return the resulting record's id + */ + create(data: object): Promise; + + /** + * Creates a new record in the App's persistent storage with the associated information + * being provided. + * + * @param data the actual data to store, must be an object otherwise it will error out + * @param association the association data which includes the model and record id + * @return the resulting record's id + */ + createWithAssociation(data: object, association: RocketChatAssociationRecord): Promise; + + /** + * Creates a new record in the App's persistent storage with the data being + * associated with more than one Rocket.Chat record. + * + * @param data the actual data to store, must be an object otherwise it will error out + * @param associations an array of association data which includes the model and record id + * @return the resulting record's id + */ + createWithAssociations(data: object, associations: Array): Promise; + + /** + * Updates an existing record with the data provided in the App's persistent storage. + * This will throw an error if the record doesn't currently exist or if the data is not an object. + * + * @param id the data record's id + * @param data the actual data to store, must be an object otherwise it will error out + * @param upsert whether a record should be created if the id to be updated does not exist + * @return the id of the updated/upserted record + */ + update(id: string, data: object, upsert?: boolean): Promise; + + /** + * Updates an existing record with the data provided in the App's persistent storage which are + * associated with provided information. + * This will throw an error if the record doesn't currently exist or if the data is not an object. + * + * @param association the association record + * @param data the actual data to store, must be an object otherwise it will error out + * @param upsert whether a record should be created if the id to be updated does not exist + * @return the id of the updated/upserted record + */ + updateByAssociation(association: RocketChatAssociationRecord, data: object, upsert?: boolean): Promise; + + /** + * Updates an existing record with the data provided in the App's persistent storage which are + * associated with more than one Rocket.Chat record. + * This will throw an error if the record doesn't currently exist or if the data is not an object. + * + * @param associations an array of association data which includes the model and record id + * @param data the actual data to store, must be an object otherwise it will error out + * @param upsert whether a record should be created if the id to be updated does not exist + * @return the id of the updated/upserted record + */ + updateByAssociations(associations: Array, data: object, upsert?: boolean): Promise; + + /** + * Removes a record by the provided id and returns the removed record. + * + * @param id of the record to remove + * @return the data record which was removed + */ + remove(id: string): Promise; + + /** + * Removes all of the records in persistent storage which are associated with the provided information. + * + * @param association the information about the association for the records to be removed + * @return the data of the removed records + */ + removeByAssociation(association: RocketChatAssociationRecord): Promise>; + + /** + * Removes all of the records in persistent storage which are associated with the provided information. + * More than one association acts like an AND which means a record in persistent storage must have all + * of the associations to be considered a match. + * + * @param associations the information about the associations for the records to be removed + * @return the data of the removed records + */ + removeByAssociations(associations: Array): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IPersistenceRead.ts b/packages/apps-engine/src/definition/accessors/IPersistenceRead.ts new file mode 100644 index 000000000000..d0d1178a44d0 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IPersistenceRead.ts @@ -0,0 +1,40 @@ +import type { RocketChatAssociationRecord } from '../metadata'; + +/** + * Provides a read-only accessor for the App's persistent storage. + * A App only has access to its own persistent storage and does not + * have access to any other App's. + */ +export interface IPersistenceRead { + /** + * Retrieves a record from the App's persistent storage by the provided id. + * A "falsey" value (undefined or null or false) is returned should nothing exist + * in the storage by the provided id. + * + * @param id the record to get's id + * @return the record if it exists, falsey if not + */ + read(id: string): Promise; + + /** + * Retrieves a record from the App's persistent storage by the provided id. + * An empty array is returned should there be no records associated with the + * data provided. + * + * @param association the association record to query the persistent storage for + * @return array of the records if any exists, empty array if none exist + */ + readByAssociation(association: RocketChatAssociationRecord): Promise>; + + /** + * Retrieves a record from the App's persistent storage by the provided id. + * Providing more than one association record acts like an AND which means a record + * in persistent storage must have all of the associations to be considered a match. + * An empty array is returned should there be no records associated with the + * data provided. + * + * @param associations the association records to query the persistent storage for + * @return array of the records if any exists, empty array if none exist + */ + readByAssociations(associations: Array): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IRead.ts b/packages/apps-engine/src/definition/accessors/IRead.ts new file mode 100644 index 000000000000..17f66d6218dc --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRead.ts @@ -0,0 +1,52 @@ +import type { ICloudWorkspaceRead } from './ICloudWorkspaceRead'; +import type { IEnvironmentRead } from './IEnvironmentRead'; +import type { ILivechatRead } from './ILivechatRead'; +import type { IMessageRead } from './IMessageRead'; +import type { INotifier } from './INotifier'; +import type { IOAuthAppsReader } from './IOAuthAppsReader'; +import type { IPersistenceRead } from './IPersistenceRead'; +import type { IRoleRead } from './IRoleRead'; +import type { IRoomRead } from './IRoomRead'; +import type { IThreadRead } from './IThreadRead'; +import type { IUploadRead } from './IUploadRead'; +import type { IUserRead } from './IUserRead'; +import type { IVideoConferenceRead } from './IVideoConferenceRead'; + +/** + * The IRead accessor provides methods for accessing the + * Rocket.Chat's environment in a read-only-fashion. + * It is safe to be injected in multiple places, idempotent and extensible + */ +export interface IRead { + /** Gets the IEnvironmentRead instance, contains settings and environmental variables. */ + getEnvironmentReader(): IEnvironmentRead; + + /** Gets the IThreadRead instance */ + + getThreadReader(): IThreadRead; + + /** Gets the IMessageRead instance. */ + getMessageReader(): IMessageRead; + + /** Gets the IPersistenceRead instance. */ + getPersistenceReader(): IPersistenceRead; + + /** Gets the IRoomRead instance. */ + getRoomReader(): IRoomRead; + + /** Gets the IUserRead instance. */ + getUserReader(): IUserRead; + + /** Gets the INotifier for notifying users/rooms. */ + getNotifier(): INotifier; + + getLivechatReader(): ILivechatRead; + getUploadReader(): IUploadRead; + getCloudWorkspaceReader(): ICloudWorkspaceRead; + + getVideoConferenceReader(): IVideoConferenceRead; + + getOAuthAppsReader(): IOAuthAppsReader; + + getRoleReader(): IRoleRead; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoleRead.ts b/packages/apps-engine/src/definition/accessors/IRoleRead.ts new file mode 100644 index 000000000000..fb56ed306a32 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoleRead.ts @@ -0,0 +1,27 @@ +import type { IRole } from '../roles'; + +/** + * Interface for reading roles. + */ +export interface IRoleRead { + /** + * Retrieves a role by its id or name. + * @param idOrName The id or name of the role to retrieve. + * @param appId The id of the app. + * @returns The role, if found. + * @returns null if no role is found. + * @throws If there is an error while retrieving the role. + */ + getOneByIdOrName(idOrName: IRole['id'] | IRole['name'], appId: string): Promise; + + /** + * Retrieves all custom roles. + * @param appId The id of the app. + * @returns All custom roles. + * @throws If there is an error while retrieving the roles. + * @throws If the app does not have the necessary permissions. + * @see IRole.protected + * @see AppPermissions.role.read + */ + getCustomRoles(appId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoomBuilder.ts b/packages/apps-engine/src/definition/accessors/IRoomBuilder.ts new file mode 100644 index 000000000000..b92955896380 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoomBuilder.ts @@ -0,0 +1,186 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom, RoomType } from '../rooms'; +import type { IUser } from '../users'; + +/** + * Interface for building out a room. + * Please note, a room creator, name, and type must be set otherwise you will NOT + * be able to successfully save the room object. + */ +export interface IRoomBuilder { + kind: RocketChatAssociationModel.ROOM | RocketChatAssociationModel.DISCUSSION; + + /** + * Provides a convient way to set the data for the room. + * Note: Providing an "id" field here will be ignored. + * + * @param room the room data to set + */ + setData(room: Partial): IRoomBuilder; + + /** + * Sets the display name of this room. + * + * @param name the display name of the room + */ + setDisplayName(name: string): IRoomBuilder; + + /** + * Gets the display name of this room. + */ + getDisplayName(): string; + + /** + * Sets the slugified name of this room, it must align to the rules of Rocket.Chat room + * names otherwise there will be an error thrown (no spaces, special characters, etc). + * + * @param name the slugified name + */ + setSlugifiedName(name: string): IRoomBuilder; + + /** + * Gets the slugified name of this room. + */ + getSlugifiedName(): string; + + /** + * Sets the room's type. + * + * @param type the room type + */ + setType(type: RoomType): IRoomBuilder; + + /** + * Gets the room's type. + */ + getType(): RoomType; + + /** + * Sets the creator of the room. + * + * @param creator the user who created the room + */ + setCreator(creator: IUser): IRoomBuilder; + + /** + * Gets the room's creator. + */ + getCreator(): IUser; + + /** + * Adds a user to the room, these are by username until further notice. + * + * @param username the user's username to add to the room + * @deprecated in favor of `addMemberToBeAddedByUsername`. This method will be removed on version 2.0.0 + */ + addUsername(username: string): IRoomBuilder; + + /** + * Sets the usernames of who are joined to the room. + * + * @param usernames the list of usernames + * @deprecated in favor of `setMembersByUsernames`. This method will be removed on version 2.0.0 + */ + setUsernames(usernames: Array): IRoomBuilder; + + /** + * Gets the usernames of users in the room. + * @deprecated in favor of `getMembersUsernames`. This method will be removed on version 2.0.0 + */ + getUsernames(): Array; + + /** + * Adds a member to the room by username + * + * @param username the user's username to add to the room + */ + addMemberToBeAddedByUsername(username: string): IRoomBuilder; + + /** + * Sets a list of members to the room by usernames + * + * @param usernames the list of usernames + */ + setMembersToBeAddedByUsernames(usernames: Array): IRoomBuilder; + + /** + * Gets the list of usernames of the members who are been added to the room + */ + getMembersToBeAddedUsernames(): Array; + + /** + * Sets whether this room should be a default room or not. + * This means that new users will automatically join this room + * when they join the server. + * + * @param isDefault room should be default or not + */ + setDefault(isDefault: boolean): IRoomBuilder; + + /** + * Gets whether this room is a default room or not. + */ + getIsDefault(): boolean; + + /** + * Sets whether this room should be in read only state or not. + * This means that users without the required permission to talk when + * a room is muted will not be able to talk but instead will only be + * able to read the contents of the room. + * + * @param isReadOnly whether it should be read only or not + */ + setReadOnly(isReadOnly: boolean): IRoomBuilder; + + /** + * Gets whether this room is on read only state or not. + */ + getIsReadOnly(): boolean; + + /** + * Sets whether this room should display the system messages (like user join, etc) + * or not. This means that whenever a system event, such as joining or leaving, happens + * then Rocket.Chat won't send the message to the channel. + * + * @param displaySystemMessages whether the messages should display or not + */ + setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder; + + /** + * Gets whether this room should display the system messages or not. + */ + getDisplayingOfSystemMessages(): boolean; + + /** + * Adds a custom field to the room. + * Note: This will replace an existing field with the same key should it exist already. + * + * @param key the name of the key + * @param value the value of the custom field + */ + addCustomField(key: string, value: object): IRoomBuilder; + + /** + * Sets the entire custom field property to an object provided. This will overwrite + * every existing key/values which are unrecoverable. + * + * @param fields the data to set + */ + setCustomFields(fields: { [key: string]: object }): IRoomBuilder; + + /** + * Gets the custom field property of the room. + */ + getCustomFields(): { [key: string]: object }; + + /** + * Gets user ids of members from a direct message + */ + getUserIds(): Array; + + /** + * Gets the resulting room that has been built up to the point of calling this method. + * Note: modifying the returned value will have no effect. + */ + getRoom(): IRoom; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoomExtender.ts b/packages/apps-engine/src/definition/accessors/IRoomExtender.ts new file mode 100644 index 000000000000..4135c63edd13 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoomExtender.ts @@ -0,0 +1,40 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; + +export interface IRoomExtender { + kind: RocketChatAssociationModel.ROOM; + + /** + * Adds a custom field to the room. + * Note: This key can not already exist or it will throw an error. + * Note: The key must not contain a period in it, an error will be thrown. + * + * @param key the name of the custom field + * @param value the value of this custom field + */ + addCustomField(key: string, value: any): IRoomExtender; + + /** + * Adds a user to the room. + * + * @param user the user which is to be added to the room + */ + addMember(user: IUser): IRoomExtender; + + /** + * Get a list of users being added to the room. + */ + getMembersBeingAdded(): Array; + + /** + * Get a list of usernames of users being added to the room. + */ + getUsernamesOfMembersBeingAdded(): Array; + + /** + * Gets the resulting room that has been extended at the point of calling this. + * Note: modifying the returned value will have no effect. + */ + getRoom(): IRoom; +} diff --git a/packages/apps-engine/src/definition/accessors/IRoomRead.ts b/packages/apps-engine/src/definition/accessors/IRoomRead.ts new file mode 100644 index 000000000000..f4e0df33239d --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IRoomRead.ts @@ -0,0 +1,93 @@ +import type { GetMessagesOptions } from '../../server/bridges/RoomBridge'; +import type { IMessageRaw } from '../messages/index'; +import type { IRoom } from '../rooms/index'; +import type { IUser } from '../users/index'; + +/** + * This accessor provides methods for accessing + * rooms in a read-only-fashion. + */ +export interface IRoomRead { + /** + * Gets a room by an id. + * + * @param id the id of the room + * @returns the room + */ + getById(id: string): Promise; + + /** + * Gets just the creator of the room by the room's id. + * + * @param id the id of the room + * @returns the creator of the room + */ + getCreatorUserById(id: string): Promise; + + /** + * Gets a room by its name. + * + * @param name the name of the room + * @returns the room + */ + getByName(name: string): Promise; + + /** + * Gets just the creator of the room by the room's name. + * + * @param name the name of the room + * @returns the creator of the room + */ + getCreatorUserByName(name: string): Promise; + + /** + * Retrieves an array of messages from the specified room. + * + * @param roomId The unique identifier of the room from which to retrieve messages. + * @param options Optional parameters for retrieving messages: + * - limit: The maximum number of messages to retrieve. Maximum 100 + * - skip: The number of messages to skip (for pagination). + * - sort: An object defining the sorting order of the messages. Each key is a field to sort by, and the value is either "asc" for ascending order or "desc" for descending order. + * @returns A Promise that resolves to an array of IMessage objects representing the messages in the room. + */ + getMessages(roomId: string, options?: Partial): Promise>; + + /** + * Gets an iterator for all of the users in the provided room. + * + * @param roomId the room's id + * @returns an iterator for the users in the room + */ + getMembers(roomId: string): Promise>; + + /** + * Gets a direct room with all usernames + * @param usernames all usernames belonging to the direct room + * @returns the room + */ + getDirectByUsernames(usernames: Array): Promise; + + /** + * Get a list of the moderators of a given room + * + * @param roomId the room's id + * @returns a list of the users with the moderator role in the room + */ + getModerators(roomId: string): Promise>; + + /** + * Get a list of the owners of a given room + * + * @param roomId the room's id + * @returns a list of the users with the owner role in the room + */ + getOwners(roomId: string): Promise>; + + /** + * Get a list of the leaders of a given room + * + * @param roomId the room's id + * @returns a list of the users with the leader role in the room + */ + getLeaders(roomId: string): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts b/packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts new file mode 100644 index 000000000000..fc003e34b587 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISchedulerExtend.ts @@ -0,0 +1,11 @@ +import type { IProcessor } from '../scheduler'; + +export interface ISchedulerExtend { + /** + * Register processors that can be scheduled to run + * + * @param {Array} processors An array of processors + * @returns List of task ids run at startup, or void no startup run is set + */ + registerProcessors(processors: Array): Promise>; +} diff --git a/packages/apps-engine/src/definition/accessors/ISchedulerModify.ts b/packages/apps-engine/src/definition/accessors/ISchedulerModify.ts new file mode 100644 index 000000000000..04faa8700799 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISchedulerModify.ts @@ -0,0 +1,31 @@ +import type { IOnetimeSchedule, IRecurringSchedule } from '../scheduler'; + +/** + * This accessor provides methods to work with the Job Scheduler + */ +export interface ISchedulerModify { + /** + * Schedules a registered processor to run _once_. + * + * @param {IOnetimeSchedule} job + * @returns jobid as string + */ + scheduleOnce(job: IOnetimeSchedule): Promise; + /** + * Schedules a registered processor to run in recurrencly according to a given interval + * + * @param {IRecurringSchedule} job + * @returns jobid as string + */ + scheduleRecurring(job: IRecurringSchedule): Promise; + /** + * Cancels a running job given its jobId + * + * @param {string} jobId + */ + cancelJob(jobId: string): Promise; + /** + * Cancels all the running jobs from the app + */ + cancelAllJobs(): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IServerSettingRead.ts b/packages/apps-engine/src/definition/accessors/IServerSettingRead.ts new file mode 100644 index 000000000000..ecb7af279241 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IServerSettingRead.ts @@ -0,0 +1,43 @@ +import type { ISetting } from '../settings/ISetting'; + +/** + * Reader for the settings inside of the server (Rocket.Chat). + * Only a subset of them are exposed to Apps. + */ +export interface IServerSettingRead { + /** + * Gets a server setting by id. + * Please note: a error will be thrown if not found + * or trying to access one that isn't exposed. + * + * @param id the id of the setting to get + * @return the setting + */ + getOneById(id: string): Promise; + + /** + * Gets a server setting's value by id. + * Please note: a error will be thrown if not found + * or trying to access one that isn't exposed. + * + * @param id the id of the setting to get + * @return the setting's value + */ + getValueById(id: string): Promise; + + /** + * Gets all of the server settings which are exposed + * to the Apps. + * + * @return an iterator of the exposed settings + */ + getAll(): Promise>; + + /** + * Checks if the server setting for the id provided is readable, + * will return true or false and won't throw an error. + * + * @param id the server setting id + */ + isReadableById(id: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts b/packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts new file mode 100644 index 000000000000..766285c25953 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IServerSettingUpdater.ts @@ -0,0 +1,6 @@ +import type { ISetting } from '../settings/ISetting'; + +export interface IServerSettingUpdater { + updateOne(setting: ISetting): Promise; + incrementValue(id: ISetting['id'], value?: number): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts b/packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts new file mode 100644 index 000000000000..400bd5ae211f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IServerSettingsModify.ts @@ -0,0 +1,40 @@ +import type { ISetting } from '../settings'; + +/** + * This accessor provides methods to change default setting options + * of Rocket.Chat in a compatible way. It is provided during + * your App's "onEnable". + */ +export interface IServerSettingsModify { + /** + * Hides an existing settings group. + * + * @param name The technical name of the group + */ + hideGroup(name: string): Promise; + + /** + * Hides a setting. This does not influence the actual functionality (the setting will still + * have its value and can be programatically read), but the administrator will not be able to see it anymore + * + * @param id the id of the setting to hide + */ + hideSetting(id: string): Promise; + + /** + * Modifies the configured value of another setting, please use it with caution as an invalid + * setting configuration could cause a Rocket.Chat instance to become unstable. + * + * @param setting the modified setting (id must be provided) + */ + modifySetting(setting: ISetting): Promise; + + /** + * Increases the setting value by the specified amount. + * To be used only with statistic settings that track the amount of times an action has been performed + * + * @param id the id of the existing Rocket.Chat setting + * @param value how much should the count be increased by. Defaults to 1. + */ + incrementValue(id: ISetting['id'], value?: number): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISettingRead.ts b/packages/apps-engine/src/definition/accessors/ISettingRead.ts new file mode 100644 index 000000000000..142ee895b161 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISettingRead.ts @@ -0,0 +1,23 @@ +import type { ISetting } from '../settings/index'; + +/** + * This accessor provides methods for accessing + * App settings in a read-only-fashion. + */ +export interface ISettingRead { + /** + * Gets the App's setting by the provided id. + * Does not throw an error but instead will return undefined it doesn't exist. + * + * @param id the id of the setting + */ + getById(id: string): Promise; + + /** + * Gets the App's setting value by the provided id. + * Note: this will throw an error if the setting doesn't exist + * + * @param id the id of the setting value to get + */ + getValueById(id: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts new file mode 100644 index 000000000000..3826286df6c9 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISettingUpdater.ts @@ -0,0 +1,5 @@ +import type { ISetting } from '../settings/ISetting'; + +export interface ISettingUpdater { + updateValue(id: ISetting['id'], value: ISetting['value']): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISettingsExtend.ts b/packages/apps-engine/src/definition/accessors/ISettingsExtend.ts new file mode 100644 index 000000000000..249776379645 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISettingsExtend.ts @@ -0,0 +1,17 @@ +import type { ISetting } from '../settings/index'; + +/** + * This accessor provides methods for adding custom settings, + * which are displayed on your App's page. + * This is provided on initialization of your App. + */ +export interface ISettingsExtend { + /** + * Adds a setting which can be configured by an administrator. + * Settings can only be added to groups which have been provided by this App earlier + * and if a group is not provided, the setting will appear outside of a group. + * + * @param setting the setting + */ + provideSetting(setting: ISetting): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts b/packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts new file mode 100644 index 000000000000..4895e61bf96f --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISlashCommandsExtend.ts @@ -0,0 +1,16 @@ +import type { ISlashCommand } from '../slashcommands'; + +/** + * This accessor provides methods for adding custom slash commands. + * It is provided during the initialization of your App + */ + +export interface ISlashCommandsExtend { + /** + * Adds a slash command which can be used during conversations lateron. + * Should a command already exists an error will be thrown. + * + * @param slashCommand the command information + */ + provideSlashCommand(slashCommand: ISlashCommand): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts b/packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts new file mode 100644 index 000000000000..b9e3d4c3e615 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/ISlashCommandsModify.ts @@ -0,0 +1,30 @@ +import type { ISlashCommand } from '../slashcommands'; + +/** + * This accessor provides methods for modifying existing Rocket.Chat slash commands. + * It is provided during "onEnable" of your App. + */ +export interface ISlashCommandsModify { + /** + * Modifies an existing command. The command must either be your App's + * own command or a system command. One App can not modify another + * App's command. + * + * @param slashCommand the modified slash command + */ + modifySlashCommand(slashCommand: ISlashCommand): Promise; + + /** + * Renders an existing slash command un-usable. + * + * @param command the command's usage without the slash + */ + disableSlashCommand(command: string): Promise; + + /** + * Enables an existing slash command to be usable again. + * + * @param command the command's usage without the slash + */ + enableSlashCommand(command: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IThreadRead.ts b/packages/apps-engine/src/definition/accessors/IThreadRead.ts new file mode 100644 index 000000000000..72ceae996eec --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IThreadRead.ts @@ -0,0 +1,9 @@ +import type { IMessage } from '../messages/index'; + +/** + * This accessor provides methods for accessing + * Thread messages in a read-only-fashion. + */ +export interface IThreadRead { + getThreadById(id: string): Promise | undefined>; +} diff --git a/packages/apps-engine/src/definition/accessors/IUIController.ts b/packages/apps-engine/src/definition/accessors/IUIController.ts new file mode 100644 index 000000000000..be7e91f5a05b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUIController.ts @@ -0,0 +1,31 @@ +import type { Omit } from '../../lib/utils'; +import type { IUIKitErrorInteraction, IUIKitInteraction, IUIKitSurface } from '../uikit'; +import type { IUIKitContextualBarViewParam, IUIKitModalViewParam } from '../uikit/UIKitInteractionResponder'; +import type { IUser } from '../users'; + +export type IUIKitInteractionParam = Omit; +export type IUIKitErrorInteractionParam = Omit; + +export type IUIKitSurfaceViewParam = Omit & Partial>; + +export interface IUIController { + /** + * @deprecated please prefer the `openSurfaceView` method + */ + openModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + /** + * @deprecated please prefer the `updateSurfaceView` method + */ + updateModalView(view: IUIKitModalViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + /** + * @deprecated please prefer the `openSurfaceView` method + */ + openContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + /** + * @deprecated please prefer the `updateSurfaceView` method + */ + updateContextualBarView(view: IUIKitContextualBarViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + setViewError(errorInteraction: IUIKitErrorInteractionParam, context: IUIKitInteractionParam, user: IUser): Promise; + openSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser): Promise; + updateSurfaceView(view: IUIKitSurfaceViewParam, context: IUIKitInteractionParam, user: IUser): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUIExtend.ts b/packages/apps-engine/src/definition/accessors/IUIExtend.ts new file mode 100644 index 000000000000..3dca2e32809b --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUIExtend.ts @@ -0,0 +1,5 @@ +import type { IUIActionButtonDescriptor } from '../ui'; + +export interface IUIExtend { + registerButton(button: IUIActionButtonDescriptor): void; +} diff --git a/packages/apps-engine/src/definition/accessors/IUploadCreator.ts b/packages/apps-engine/src/definition/accessors/IUploadCreator.ts new file mode 100644 index 000000000000..25262b42e7d3 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUploadCreator.ts @@ -0,0 +1,12 @@ +import type { IUpload } from '../uploads'; +import type { IUploadDescriptor } from '../uploads/IUploadDescriptor'; + +export interface IUploadCreator { + /** + * Create an upload to a room + * + * @param buffer A Buffer with the file's content (See [here](https://nodejs.org/api/buffer.html) + * @param descriptor The metadata about the upload + */ + uploadBuffer(buffer: Buffer, descriptor: IUploadDescriptor): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUploadRead.ts b/packages/apps-engine/src/definition/accessors/IUploadRead.ts new file mode 100644 index 000000000000..ce4029a8c0c8 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUploadRead.ts @@ -0,0 +1,7 @@ +import type { IUpload } from '../uploads'; + +export interface IUploadRead { + getById(id: string): Promise; + getBufferById(id: string): Promise; + getBuffer(upload: IUpload): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUserBuilder.ts b/packages/apps-engine/src/definition/accessors/IUserBuilder.ts new file mode 100644 index 000000000000..4e0c52aa893a --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUserBuilder.ts @@ -0,0 +1,60 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IUser, IUserEmail } from '../users'; + +/** + * Interface for creating a user. + * Please note, a username and email provided must be unique else you will NOT + * be able to successfully save the user object. + */ +export interface IUserBuilder { + kind: RocketChatAssociationModel.USER; + + /** + * Provides a convient way to set the data for the user. + * Note: Providing an "id" field here will be ignored. + * + * @param user the user data to set + */ + setData(user: Partial): IUserBuilder; + + /** + * Sets emails of the user + * + * @param emails the array of email addresses of the user + */ + setEmails(emails: Array): IUserBuilder; + + /** + * Gets emails of the user + */ + getEmails(): Array; + + /** + * Sets the display name of this user. + * + * @param name the display name of the user + */ + setDisplayName(name: string): IUserBuilder; + + /** + * Gets the display name of this user. + */ + getDisplayName(): string; + + /** + * Sets the username for the user + * + * @param username username of the user + */ + setUsername(username: string): IUserBuilder; + + /** + * Gets the username of this user + */ + getUsername(): string; + + /** + * Gets the user + */ + getUser(): Partial; +} diff --git a/packages/apps-engine/src/definition/accessors/IUserRead.ts b/packages/apps-engine/src/definition/accessors/IUserRead.ts new file mode 100644 index 000000000000..33c4c6e455e4 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUserRead.ts @@ -0,0 +1,22 @@ +import type { IUser } from '../users/index'; + +/** + * This accessor provides methods for accessing + * users in a read-only-fashion. + */ +export interface IUserRead { + getById(id: string): Promise; + + getByUsername(username: string): Promise; + + /** + * Gets the app user of this app. + */ + getAppUser(appId?: string): Promise; + + /** + * Gets the user's badge count (unread messages count). + * @param uid user's id + */ + getUserUnreadMessageCount(uid: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IUserUpdater.ts b/packages/apps-engine/src/definition/accessors/IUserUpdater.ts new file mode 100644 index 000000000000..8c57b4dadfa8 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IUserUpdater.ts @@ -0,0 +1,18 @@ +import type { IUser } from '../users/IUser'; + +/** + * Updating a user is a more granular approach, since + * it is one of the more sensitive aspects of Rocket.Chat - + * or any other system for that matter. + * + * Allowing apps to modify _all_ the aspects of a user + * would open a critical surface for them to abuse such + * power and "take hold" of a server, for instance. + */ +export interface IUserUpdater { + updateStatusText(user: IUser, statusText: IUser['statusText']): Promise; + updateStatus(user: IUser, statusText: IUser['statusText'], status: IUser['status']): Promise; + updateBio(user: IUser, bio: IUser['bio']): Promise; + updateCustomFields(user: IUser, customFields: IUser['customFields']): Promise; + deactivate(userId: IUser['id'], confirmRelinquish: boolean): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts b/packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts new file mode 100644 index 000000000000..c61224893e11 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConfProvidersExtend.ts @@ -0,0 +1,15 @@ +import type { IVideoConfProvider } from '../videoConfProviders'; + +/** + * This accessor provides methods for adding videoconf providers. + * It is provided during the initialization of your App + */ + +export interface IVideoConfProvidersExtend { + /** + * Adds a videoconf provider + * + * @param provider the provider information + */ + provideVideoConfProvider(provider: IVideoConfProvider): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts b/packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts new file mode 100644 index 000000000000..11b96da0e4ef --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConferenceBuilder.ts @@ -0,0 +1,34 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { AppVideoConference } from '../videoConferences'; + +export interface IVideoConferenceBuilder { + kind: RocketChatAssociationModel.VIDEO_CONFERENCE; + + setData(call: Partial): IVideoConferenceBuilder; + + setRoomId(rid: string): IVideoConferenceBuilder; + + getRoomId(): string; + + setCreatedBy(userId: string): IVideoConferenceBuilder; + + getCreatedBy(): string; + + setProviderName(name: string): IVideoConferenceBuilder; + + getProviderName(): string; + + setProviderData(data: Record): IVideoConferenceBuilder; + + getProviderData(): Record; + + setTitle(name: string): IVideoConferenceBuilder; + + getTitle(): string; + + setDiscussionRid(rid: string | undefined): IVideoConferenceBuilder; + + getDiscussionRid(): string | undefined; + + getVideoConference(): AppVideoConference; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts b/packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts new file mode 100644 index 000000000000..d9b7e5838368 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConferenceExtend.ts @@ -0,0 +1,21 @@ +import type { RocketChatAssociationModel } from '../metadata'; +import type { IVideoConferenceUser, VideoConference } from '../videoConferences'; +import type { VideoConferenceMember } from '../videoConferences/IVideoConference'; + +export interface IVideoConferenceExtender { + kind: RocketChatAssociationModel.VIDEO_CONFERENCE; + + setProviderData(value: Record): IVideoConferenceExtender; + + setStatus(value: VideoConference['status']): IVideoConferenceExtender; + + setEndedBy(value: IVideoConferenceUser['_id']): IVideoConferenceExtender; + + setEndedAt(value: VideoConference['endedAt']): IVideoConferenceExtender; + + addUser(userId: VideoConferenceMember['_id'], ts?: VideoConferenceMember['ts']): IVideoConferenceExtender; + + setDiscussionRid(rid: VideoConference['discussionRid']): IVideoConferenceExtender; + + getVideoConference(): VideoConference; +} diff --git a/packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts b/packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts new file mode 100644 index 000000000000..aa2d53d70590 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/IVideoConferenceRead.ts @@ -0,0 +1,15 @@ +import type { VideoConference } from '../videoConferences/IVideoConference'; + +/** + * This accessor provides methods for accessing + * video conferences in a read-only-fashion. + */ +export interface IVideoConferenceRead { + /** + * Gets a video conference by an id. + * + * @param id the id of the video conference + * @returns the video conference + */ + getById(id: string): Promise; +} diff --git a/packages/apps-engine/src/definition/accessors/index.ts b/packages/apps-engine/src/definition/accessors/index.ts new file mode 100644 index 000000000000..e98a4208fe13 --- /dev/null +++ b/packages/apps-engine/src/definition/accessors/index.ts @@ -0,0 +1,58 @@ +export * from './IApiExtend'; +export * from './IAppAccessors'; +export * from './IAppInstallationContext'; +export * from './IAppUpdateContext'; +export * from './IAppUninstallationContext'; +export * from './ICloudWorkspaceRead'; +export * from './IConfigurationExtend'; +export * from './IConfigurationModify'; +export * from './IDiscussionBuilder'; +export * from './IEnvironmentalVariableRead'; +export * from './IEnvironmentRead'; +export * from './IEnvironmentWrite'; +export * from './IExternalComponentsExtend'; +export * from './IHttp'; +export * from './ILivechatCreator'; +export * from './ILivechatMessageBuilder'; +export * from './ILivechatRead'; +export * from './ILivechatUpdater'; +export * from './ILogEntry'; +export * from './ILogger'; +export * from './IMessageBuilder'; +export * from './IMessageExtender'; +export * from './IMessageRead'; +export * from './IMessageUpdater'; +export * from './IModify'; +export * from './IModifyCreator'; +export * from './IModifyDeleter'; +export * from './IModifyExtender'; +export * from './IModifyUpdater'; +export * from './INotifier'; +export * from './IPersistence'; +export * from './IPersistenceRead'; +export * from './IRead'; +export * from './IRoleRead'; +export * from './IRoomBuilder'; +export * from './IRoomExtender'; +export * from './IRoomRead'; +export * from './ISchedulerExtend'; +export * from './ISchedulerModify'; +export * from './IServerSettingRead'; +export * from './IServerSettingsModify'; +export * from './IServerSettingUpdater'; +export * from './ISettingRead'; +export * from './ISettingsExtend'; +export * from './ISettingUpdater'; +export * from './ISlashCommandsExtend'; +export * from './ISlashCommandsModify'; +export * from './IUIController'; +export * from './IUIExtend'; +export * from './IUploadCreator'; +export * from './IUploadRead'; +export * from './IUserBuilder'; +export * from './IUserRead'; +export * from './IVideoConferenceBuilder'; +export * from './IVideoConferenceExtend'; +export * from './IVideoConferenceRead'; +export * from './IVideoConfProvidersExtend'; +export * from './IModerationModify'; diff --git a/packages/apps-engine/src/definition/api/ApiEndpoint.ts b/packages/apps-engine/src/definition/api/ApiEndpoint.ts new file mode 100644 index 000000000000..8ab7610c9b4f --- /dev/null +++ b/packages/apps-engine/src/definition/api/ApiEndpoint.ts @@ -0,0 +1,40 @@ +import type { IApp } from '../IApp'; +import { HttpStatusCode } from '../accessors'; +import type { IApiEndpoint } from './IApiEndpoint'; +import type { IApiResponse, IApiResponseJSON } from './IResponse'; + +/** Represents an api endpoint that is being provided. */ +export abstract class ApiEndpoint implements IApiEndpoint { + /** + * The last part of the api URL. Example: https://{your-server-address}/api/apps/public/{your-app-id}/{path} + * or https://{your-server-address}/api/apps/private/{your-app-id}/{private-hash}/{path} + */ + public path: string; + + constructor(public app: IApp) {} + + /** + * Return response with status 200 (OK) and a optional content + * @param content + */ + protected success(content?: any): IApiResponse { + return { + status: HttpStatusCode.OK, + content, + }; + } + + /** + * Return a json response adding Content Type header as + * application/json if not already provided + * @param reponse + */ + protected json(response: IApiResponseJSON): IApiResponse { + if (!response.headers || !response.headers['content-type']) { + response.headers = response.headers || {}; + response.headers['content-type'] = 'application/json'; + } + + return response; + } +} diff --git a/packages/apps-engine/src/definition/api/IApi.ts b/packages/apps-engine/src/definition/api/IApi.ts new file mode 100644 index 000000000000..9ad2f42ed5a8 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApi.ts @@ -0,0 +1,58 @@ +import type { IApiEndpoint } from './IApiEndpoint'; + +/** + * Represents an api that is being provided. + */ +export interface IApi { + /** + * Provides the visibility method of the URL, see the ApiVisibility descriptions for more information + */ + visibility: ApiVisibility; + /** + * Provides the visibility method of the URL, see the ApiSecurity descriptions for more information + */ + security: ApiSecurity; + /** + * Provide enpoints for this api registry + */ + endpoints: Array; +} + +export enum ApiVisibility { + /** + * A public Api has a fixed format for a url. Using it enables an + * easy to remember structure, however, it also means the url is + * intelligently guessed. As a result, we recommend having some + * sort of security setup if you must have a public api.Whether + * you use the provided security, ApiSecurity, or implement your own. + * Url format: + * `https://{your-server-address}/api/apps/public/{your-app-id}/{path}` + */ + PUBLIC, + /** + * Private Api's contain a random value in the url format, + * making them harder go guess by default. The random value + * will be generated whenever the App is installed on a server. + * This means that the URL will not be the same on any server, + * but will remain the same throughout the lifecycle of an App + * including updates. As a result, if a user uninstalls the App + * and reinstalls the App, then the random value will change. + * Url format: + * `https://{your-server-address}/api/apps/private/{your-app-id}/{random-hash}/{path}` + */ + PRIVATE, +} + +export enum ApiSecurity { + /** + * No security check will be executed agains the calls made to this URL + */ + UNSECURE, + /** + * Only calls containing a valid token will be able to execute the api + * Mutiple tokens can be generated to access the api, by default one + * will be generated automatically. + * @param `X-Auth-Token` + */ + // CHECKSUM_SECRET, +} diff --git a/packages/apps-engine/src/definition/api/IApiEndpoint.ts b/packages/apps-engine/src/definition/api/IApiEndpoint.ts new file mode 100644 index 000000000000..b369fc175dc2 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiEndpoint.ts @@ -0,0 +1,47 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import type { IApiEndpointInfo } from './IApiEndpointInfo'; +import type { IApiExample } from './IApiExample'; +import type { IApiRequest } from './IRequest'; +import type { IApiResponse } from './IResponse'; + +/** + * Represents an api endpoint that is being provided. + */ +export interface IApiEndpoint { + /** + * The last part of the api URL. Example: https://{your-server-address}/api/apps/public/{your-app-id}/{path} + * or https://{your-server-address}/api/apps/private/{your-app-id}/{private-hash}/{path} + */ + path: string; + examples?: { [key: string]: IApiExample }; + /** + * Whether this endpoint requires an authenticated user to access it. + * + * The authentication will be done by the host server using its own + * authentication system. + * + * If no authentication is provided, the request will be automatically + * rejected with a 401 status code. + */ + authRequired?: boolean; + + /** + * The methods that are available for this endpoint. + * This property is provided by the Runtime and should not be set manually. + * + * Its values are used on the Apps-Engine to validate the request method. + */ + _availableMethods?: string[]; + + /** + * Called whenever the publically accessible url for this App is called, + * if you handle the methods differently then split it out so your code doesn't get too big. + */ + get?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + post?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + put?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + delete?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + head?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + options?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; + patch?(request: IApiRequest, endpoint: IApiEndpointInfo, read: IRead, modify: IModify, http: IHttp, persis: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/api/IApiEndpointInfo.ts b/packages/apps-engine/src/definition/api/IApiEndpointInfo.ts new file mode 100644 index 000000000000..de9144784b34 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiEndpointInfo.ts @@ -0,0 +1,6 @@ +export interface IApiEndpointInfo { + basePath: string; + fullPath: string; + appId: string; + hash?: string; +} diff --git a/packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts b/packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts new file mode 100644 index 000000000000..0ede26045f79 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiEndpointMetadata.ts @@ -0,0 +1,10 @@ +import type { IApiExample } from './IApiExample'; + +export interface IApiEndpointMetadata { + path: string; + computedPath: string; + methods: Array; + examples?: { + [key: string]: IApiExample; + }; +} diff --git a/packages/apps-engine/src/definition/api/IApiExample.ts b/packages/apps-engine/src/definition/api/IApiExample.ts new file mode 100644 index 000000000000..f870d59e643d --- /dev/null +++ b/packages/apps-engine/src/definition/api/IApiExample.ts @@ -0,0 +1,19 @@ +/** + * Represents the parameters of an api example. + */ +export interface IApiExample { + params?: { [key: string]: string }; + query?: { [key: string]: string }; + headers?: { [key: string]: string }; + content?: any; +} + +/** + * Decorator to describe api examples + */ +export function example(options: IApiExample) { + return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => { + target.examples = target.examples || {}; + target.examples[propertyKey] = options; + }; +} diff --git a/packages/apps-engine/src/definition/api/IRequest.ts b/packages/apps-engine/src/definition/api/IRequest.ts new file mode 100644 index 000000000000..027de11b2026 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IRequest.ts @@ -0,0 +1,16 @@ +import type { RequestMethod } from '../accessors'; +import type { IUser } from '../users'; + +export interface IApiRequest { + method: RequestMethod; + headers: { [key: string]: string }; + query: { [key: string]: string }; + params: { [key: string]: string }; + content: any; + privateHash?: string; + /** + * The user that is making the request, as + * authenticated by Rocket.Chat's strategy. + */ + user?: IUser; +} diff --git a/packages/apps-engine/src/definition/api/IResponse.ts b/packages/apps-engine/src/definition/api/IResponse.ts new file mode 100644 index 000000000000..8f394b8a93b0 --- /dev/null +++ b/packages/apps-engine/src/definition/api/IResponse.ts @@ -0,0 +1,13 @@ +import type { HttpStatusCode } from '../accessors'; + +export interface IApiResponse { + status: HttpStatusCode; + headers?: { [key: string]: string }; + content?: any; +} + +export interface IApiResponseJSON { + status: HttpStatusCode; + headers?: { [key: string]: string }; + content?: { [key: string]: any }; +} diff --git a/packages/apps-engine/src/definition/api/index.ts b/packages/apps-engine/src/definition/api/index.ts new file mode 100644 index 000000000000..41e4482f2ec6 --- /dev/null +++ b/packages/apps-engine/src/definition/api/index.ts @@ -0,0 +1,8 @@ +export { ApiEndpoint } from './ApiEndpoint'; +export { IApi, ApiVisibility, ApiSecurity } from './IApi'; +export { IApiEndpoint } from './IApiEndpoint'; +export { IApiEndpointInfo } from './IApiEndpointInfo'; +export { IApiExample, example } from './IApiExample'; +export { IApiRequest } from './IRequest'; +export { IApiResponse } from './IResponse'; +export { IApiEndpointMetadata } from './IApiEndpointMetadata'; diff --git a/packages/apps-engine/src/definition/app-schema.json b/packages/apps-engine/src/definition/app-schema.json new file mode 100644 index 000000000000..68c2e0c19edc --- /dev/null +++ b/packages/apps-engine/src/definition/app-schema.json @@ -0,0 +1,75 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rocket.Chat App", + "description": "A Rocket.Chat App declaration for usage inside of Rocket.Chat.", + "type": "object", + "properties": { + "id": { + "description": "The App's unique identifier in uuid v4 format. This is optional, although recommended, however if you are going to publish on the App store, you will be assigned one.", + "type": "string", + "pattern": "^[0-9a-fA-f]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$", + "minLength": 36, + "maxLength": 36 + }, + "name": { + "description": "The public visible name of this App.", + "type": "string" + }, + "nameSlug": { + "description": "A url friendly slugged version of your App's name.", + "type": "string", + "pattern": "^([a-z]|\\-)+$", + "minLength": 3 + }, + "version": { + "description": "The version of this App which will be used for display publicly and letting users know there is an update. This uses the semver format.", + "type": "string", + "pattern": "^(?:\\d*)\\.(?:\\d*)\\.(?:\\d*)$", + "minLength": 5 + }, + "description": { + "description": "A description of this App, used to explain what this App does and provides for the user.", + "type": "string" + }, + "requiredApiVersion": { + "description": "The required version of the App's API which this App depends on. This uses the semver format.", + "type": "string", + "pattern": "^(?:\\^|~)?(?:\\d*)\\.(?:\\d*)\\.(?:\\d*)$", + "minLength": 5 + }, + "author": { + "type": "object", + "properties": { + "name": { + "description": "The author's name who created this App.", + "type": "string" + }, + "support": { + "description": "The place where people can get support for this App, whether email or website.", + "type": "string" + }, + "homepage": { + "description": "The homepage for this App, it can be a Github or the author's website.", + "type": "string", + "format": "uri" + } + }, + "required": ["name", "support"] + }, + "classFile": { + "type": "string", + "description": "The name of the file which contains your App TypeScript source code.", + "pattern": "^.*\\.(ts)$" + }, + "iconFile": { + "type": "string", + "description": "The name of the file to use as the icon.", + "pattern": "^.*\\.(png|jpg|jpeg|gif)$" + }, + "assetsFolder": { + "type": "string", + "description": "The name of the folder which contains all of your resources, it should not start with a period." + } + }, + "required": ["id", "name", "nameSlug", "version", "description", "requiredApiVersion", "author", "classFile", "iconFile"] +} diff --git a/packages/apps-engine/src/definition/assets/IAsset.ts b/packages/apps-engine/src/definition/assets/IAsset.ts new file mode 100644 index 000000000000..30c54ae6565d --- /dev/null +++ b/packages/apps-engine/src/definition/assets/IAsset.ts @@ -0,0 +1,6 @@ +export interface IAsset { + name: string; + path: string; + type: string; + public: boolean; +} diff --git a/packages/apps-engine/src/definition/assets/IAssetProvider.ts b/packages/apps-engine/src/definition/assets/IAssetProvider.ts new file mode 100644 index 000000000000..4e1da50222fa --- /dev/null +++ b/packages/apps-engine/src/definition/assets/IAssetProvider.ts @@ -0,0 +1,5 @@ +import type { IAsset } from './IAsset'; + +export interface IAssetProvider { + getAssets(): Array; +} diff --git a/packages/apps-engine/src/definition/assets/index.ts b/packages/apps-engine/src/definition/assets/index.ts new file mode 100644 index 000000000000..98abab64ddf5 --- /dev/null +++ b/packages/apps-engine/src/definition/assets/index.ts @@ -0,0 +1,4 @@ +import { IAsset } from './IAsset'; +import { IAssetProvider } from './IAssetProvider'; + +export { IAsset, IAssetProvider }; diff --git a/packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts b/packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts new file mode 100644 index 000000000000..40a46bf7e37f --- /dev/null +++ b/packages/apps-engine/src/definition/cloud/IWorkspaceToken.ts @@ -0,0 +1,4 @@ +export interface IWorkspaceToken { + token: string; + expiresAt: Date; +} diff --git a/packages/apps-engine/src/definition/email/IEmail.ts b/packages/apps-engine/src/definition/email/IEmail.ts new file mode 100644 index 000000000000..ca81b23e5bcc --- /dev/null +++ b/packages/apps-engine/src/definition/email/IEmail.ts @@ -0,0 +1,9 @@ +export interface IEmail { + to: string | string[]; + from: string; + replyTo?: string; + subject: string; + html?: string; + text?: string; + headers?: string; +} diff --git a/packages/apps-engine/src/definition/email/IEmailDescriptor.ts b/packages/apps-engine/src/definition/email/IEmailDescriptor.ts new file mode 100644 index 000000000000..168bae039168 --- /dev/null +++ b/packages/apps-engine/src/definition/email/IEmailDescriptor.ts @@ -0,0 +1,11 @@ +export interface IEmailDescriptor { + from?: string | undefined; + to?: string | Array | undefined; + cc?: string | Array | undefined; + bcc?: string | Array | undefined; + replyTo?: string | Array | undefined; + subject?: string | undefined; + text?: string | undefined; + html?: string | undefined; + headers?: Record | undefined; +} diff --git a/packages/apps-engine/src/definition/email/IPreEmailSent.ts b/packages/apps-engine/src/definition/email/IPreEmailSent.ts new file mode 100644 index 000000000000..2d5e40c92851 --- /dev/null +++ b/packages/apps-engine/src/definition/email/IPreEmailSent.ts @@ -0,0 +1,25 @@ +import type { IEmailDescriptor, IPreEmailSentContext } from '.'; +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; + +/** + * Event interface that allows apps to + * register as a handler of of the `IPreEmailSent` + * event. + * + * This event is trigger before the mailer sends + * an email. + * + * To prevent the email from being sent, you can + * throw an error with a message specifying the + * reason for rejection. + */ +export interface IPreEmailSent { + [AppMethod.EXECUTE_PRE_EMAIL_SENT]( + context: IPreEmailSentContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/email/IPreEmailSentContext.ts b/packages/apps-engine/src/definition/email/IPreEmailSentContext.ts new file mode 100644 index 000000000000..7427424f88eb --- /dev/null +++ b/packages/apps-engine/src/definition/email/IPreEmailSentContext.ts @@ -0,0 +1,6 @@ +import type { IEmailDescriptor } from './IEmailDescriptor'; + +export interface IPreEmailSentContext { + context: unknown; + email: IEmailDescriptor; +} diff --git a/packages/apps-engine/src/definition/email/index.ts b/packages/apps-engine/src/definition/email/index.ts new file mode 100644 index 000000000000..6074ebaec4c3 --- /dev/null +++ b/packages/apps-engine/src/definition/email/index.ts @@ -0,0 +1,4 @@ +export * from './IEmailDescriptor'; +export * from './IPreEmailSent'; +export * from './IPreEmailSentContext'; +export * from './IEmail'; diff --git a/packages/apps-engine/src/definition/example-app.json b/packages/apps-engine/src/definition/example-app.json new file mode 100644 index 000000000000..e78048d1d316 --- /dev/null +++ b/packages/apps-engine/src/definition/example-app.json @@ -0,0 +1,13 @@ +{ + //This is an example of how a app.json file will look like + "name": "Testing", + "nameSlug": "testing", + "description": "Testing description", + "version": "1.0.0", + "requiredApiVersion": "0.0.1", + "author": { + "name": "Bradley Hilton", + "support": "https://github.com/RocketChat/Rocket.Chat.Apps-engine" + }, + "classFile": "ExampleApp.ts" +} diff --git a/packages/apps-engine/src/definition/exceptions/AppsEngineException.ts b/packages/apps-engine/src/definition/exceptions/AppsEngineException.ts new file mode 100644 index 000000000000..a3e802aa69d8 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/AppsEngineException.ts @@ -0,0 +1,32 @@ +/** + * The internal exception from the framework + * + * It's used to signal to the outside world that + * a _known_ exception has happened during the execution + * of the apps. + * + * It's the base exception for other known classes + * such as UserNotAllowedException, which is used + * to inform the host that an app identified + * that a user cannot perform some action, e.g. + * join a room + */ +export class AppsEngineException extends Error { + public name = 'AppsEngineException'; + + public static JSONRPC_ERROR_CODE = -32070; + + public message: string; + + constructor(message?: string) { + super(); + this.message = message; + } + + public getErrorInfo() { + return { + name: this.name, + message: this.message, + }; + } +} diff --git a/packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts b/packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts new file mode 100644 index 000000000000..e5043d93e336 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/EssentialAppDisabledException.ts @@ -0,0 +1,16 @@ +import { AppsEngineException } from '.'; + +/** + * This exception informs the host system that an + * app essential to the execution of a system action + * is disabled, so the action should be halted. + * + * Apps can register to be considered essential to + * the execution of internal events of the framework + * such as `IPreMessageSentPrevent`, `IPreRoomUserJoined`, + * etc. + * + * This is used interally by the framework and is not + * intended to be thrown manually by apps. + */ +export class EssentialAppDisabledException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts b/packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts new file mode 100644 index 000000000000..0ae9d98edb3f --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/FileUploadNotAllowedException.ts @@ -0,0 +1,12 @@ +import { AppsEngineException } from './AppsEngineException'; + +/** + * This exception informs the host system that an + * app has determined that a file upload is not + * allowed to be completed. + * + * Currently it is expected to be thrown by the + * following events: + * - IPreFileUpload + */ +export class FileUploadNotAllowedException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts b/packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts new file mode 100644 index 000000000000..2b1a193accb2 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/InvalidSettingValueException.ts @@ -0,0 +1,8 @@ +import { AppsEngineException } from './AppsEngineException'; + +/** + * This exception informs the host system that an + * app has determined that an invalid setting value + * is passed. + */ +export class InvalidSettingValueException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts b/packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts new file mode 100644 index 000000000000..d81969d8f62a --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/UserNotAllowedException.ts @@ -0,0 +1,14 @@ +import { AppsEngineException } from '.'; + +/** + * This exception informs the host system that an + * app has determined that an user is not allowed + * to perform a specific action. + * + * Currently it is expected to be thrown by the + * following events: + * - IPreRoomCreatePrevent + * - IPreRoomUserJoined + * - IPreRoomUserLeave + */ +export class UserNotAllowedException extends AppsEngineException {} diff --git a/packages/apps-engine/src/definition/exceptions/index.ts b/packages/apps-engine/src/definition/exceptions/index.ts new file mode 100644 index 000000000000..6129978c2f89 --- /dev/null +++ b/packages/apps-engine/src/definition/exceptions/index.ts @@ -0,0 +1,5 @@ +export * from './AppsEngineException'; +export * from './EssentialAppDisabledException'; +export * from './UserNotAllowedException'; +export * from './FileUploadNotAllowedException'; +export * from './InvalidSettingValueException'; diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts new file mode 100644 index 000000000000..7c750e1c7e22 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponent.ts @@ -0,0 +1,51 @@ +import type { IExternalComponentOptions } from './IExternalComponentOptions'; +import type { IExternalComponentState } from './IExternalComponentState'; +/** + * Represents an external component that is being provided. + */ +export interface IExternalComponent { + /** + * Provides the appId of the app which the external component belongs to. + */ + appId: string; + /** + * Provides the name of the external component. This key must be unique. + */ + name: string; + /** + * Provides the description of the external component. + */ + description: string; + /** + * Provides the icon's url or base64 string. + */ + icon: string; + /** + * Provides the location which external component needs + * to register, see the ExternalComponentLocation descriptions + * for the more information. + */ + location: ExternalComponentLocation; + /** + * Provides the url that external component will load. + */ + url: string; + /** + * Provides options for the external component. + */ + options?: IExternalComponentOptions; + /** + * Represents the current state of the external component. + * The value is *null* until the ExternalComponentOpened + * event is triggered. It doesn't make sense to get its value in + * PreExternalComponentOpenedPrevent, PreExternalComponentOpenedModify + * and PreExternalComponentOpenedExtend handlers. + */ + state?: IExternalComponentState; +} + +export enum ExternalComponentLocation { + CONTEXTUAL_BAR = 'CONTEXTUAL_BAR', + + MODAL = 'MODAL', +} diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts new file mode 100644 index 000000000000..2581c047ab43 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentOptions.ts @@ -0,0 +1,10 @@ +export interface IExternalComponentOptions { + /** + * The width of the external component + */ + width?: number; + /** + * The height of the external component + */ + height?: number; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts b/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts new file mode 100644 index 000000000000..4c401f3e28d1 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IExternalComponentState.ts @@ -0,0 +1,16 @@ +import type { IExternalComponentRoomInfo, IExternalComponentUserInfo } from '../../client/definition'; + +/** + * The state of an external component, which contains the + * current user's information and the current room's information. + */ +export interface IExternalComponentState { + /** + * The user who opened this external component + */ + currentUser: IExternalComponentUserInfo; + /** + * The room where the external component belongs to + */ + currentRoom: IExternalComponentRoomInfo; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts new file mode 100644 index 000000000000..24d224ba2913 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentClosed.ts @@ -0,0 +1,16 @@ +import type { IHttp, IPersistence, IRead } from '../accessors'; +import type { IExternalComponent } from './IExternalComponent'; + +/** + * Handler called after an external component is closed. + */ +export interface IPostExternalComponentClosed { + /** + * Method called after an external component is closed. + * + * @param externalComponent The external component which was closed + * @param read An accessor to the environment + * @param http An accessor to the outside world + */ + executePostExternalComponentClosed(externalComponent: IExternalComponent, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts new file mode 100644 index 000000000000..8a09ebe711d2 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/IPostExternalComponentOpened.ts @@ -0,0 +1,16 @@ +import type { IHttp, IPersistence, IRead } from '../accessors'; +import type { IExternalComponent } from './IExternalComponent'; + +/** + * Handler called after an external component is opened. + */ +export interface IPostExternalComponentOpened { + /** + * Method called after an external component is opened. + * + * @param externalComponent The external component which was opened + * @param read An accessor to the environment + * @param http An accessor to the outside world + */ + executePostExternalComponentOpened(externalComponent: IExternalComponent, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/externalComponent/index.ts b/packages/apps-engine/src/definition/externalComponent/index.ts new file mode 100644 index 000000000000..acd4bbf44982 --- /dev/null +++ b/packages/apps-engine/src/definition/externalComponent/index.ts @@ -0,0 +1,5 @@ +import { IExternalComponent } from './IExternalComponent'; +import { IPostExternalComponentClosed } from './IPostExternalComponentClosed'; +import { IPostExternalComponentOpened } from './IPostExternalComponentOpened'; + +export { IExternalComponent, IPostExternalComponentClosed, IPostExternalComponentOpened }; diff --git a/packages/apps-engine/src/definition/livechat/IDepartment.ts b/packages/apps-engine/src/definition/livechat/IDepartment.ts new file mode 100644 index 000000000000..1a59c9835612 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IDepartment.ts @@ -0,0 +1,17 @@ +export interface IDepartment { + id: string; + name?: string; + email?: string; + description?: string; + offlineMessageChannelName?: string; + requestTagBeforeClosingChat?: false; + chatClosingTags?: Array; + abandonedRoomsCloseCustomMessage?: string; + waitingQueueMessage?: string; + departmentsAllowedToForward?: string; + enabled: boolean; + updatedAt: Date; + numberOfAgents: number; + showOnOfflineForm: boolean; + showOnRegistration: boolean; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts new file mode 100644 index 000000000000..b94f07ef0250 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatEventContext.ts @@ -0,0 +1,7 @@ +import type { IUser } from '../users'; +import type { ILivechatRoom } from './ILivechatRoom'; + +export interface ILivechatEventContext { + agent: IUser; + room: ILivechatRoom; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatMessage.ts b/packages/apps-engine/src/definition/livechat/ILivechatMessage.ts new file mode 100644 index 000000000000..d7cc5497d70e --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatMessage.ts @@ -0,0 +1,7 @@ +import type { IMessage } from '../messages/IMessage'; +import type { IVisitor } from './IVisitor'; + +export interface ILivechatMessage extends IMessage { + visitor?: IVisitor; + token?: string; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatRoom.ts b/packages/apps-engine/src/definition/livechat/ILivechatRoom.ts new file mode 100644 index 000000000000..e3f55142331a --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatRoom.ts @@ -0,0 +1,55 @@ +import { RoomType } from '../rooms'; +import type { IRoom } from '../rooms/IRoom'; +import type { IUser } from '../users'; +import type { IDepartment } from './IDepartment'; +import type { IVisitor } from './IVisitor'; + +export enum OmnichannelSourceType { + WIDGET = 'widget', + EMAIL = 'email', + SMS = 'sms', + APP = 'app', + OTHER = 'other', +} + +interface IOmnichannelSourceApp { + type: 'app'; + id: string; + // A human readable alias that goes with the ID, for post analytical purposes + alias?: string; + // A label to be shown in the room info + label?: string; + sidebarIcon?: string; + defaultIcon?: string; +} +type OmnichannelSource = + | { + type: Exclude; + } + | IOmnichannelSourceApp; + +export interface IVisitorChannelInfo { + lastMessageTs?: Date; + phone?: string; +} + +export interface ILivechatRoom extends IRoom { + visitor: IVisitor; + visitorChannelInfo?: IVisitorChannelInfo; + department?: IDepartment; + closer: 'user' | 'visitor' | 'bot'; + closedBy?: IUser; + servedBy?: IUser; + responseBy?: IUser; + isWaitingResponse: boolean; + isOpen: boolean; + closedAt?: Date; + source?: OmnichannelSource; +} + +export const isLivechatRoom = (room: IRoom): room is ILivechatRoom => { + return room.type === RoomType.LIVE_CHAT; +}; +export const isLivechatFromApp = (room: ILivechatRoom): room is ILivechatRoom & { source: IOmnichannelSourceApp } => { + return room.source && room.source.type === 'app'; +}; diff --git a/packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts b/packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts new file mode 100644 index 000000000000..dba672ad391b --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatRoomClosedHandler.ts @@ -0,0 +1,19 @@ +import type { IHttp, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after a livechat room is closed. + * @deprecated please prefer the IPostLivechatRoomClosed event + */ +export interface ILivechatRoomClosedHandler { + /** + * Method called *after* a livechat room is closed. + * + * @param livechatRoom The livechat room which is closed. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persistence An accessor to the App's persistence + */ + [AppMethod.EXECUTE_LIVECHAT_ROOM_CLOSED_HANDLER](data: ILivechatRoom, read: IRead, http: IHttp, persistence: IPersistence): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts b/packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts new file mode 100644 index 000000000000..988d0a2ec5fd --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatTransferData.ts @@ -0,0 +1,8 @@ +import type { IUser } from '../users'; +import type { ILivechatRoom } from './ILivechatRoom'; + +export interface ILivechatTransferData { + currentRoom: ILivechatRoom; + targetAgent?: IUser; + targetDepartment?: string; +} diff --git a/packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts b/packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts new file mode 100644 index 000000000000..74db472751d4 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/ILivechatTransferEventContext.ts @@ -0,0 +1,15 @@ +import type { IRoom } from '../rooms'; +import type { IUser } from '../users'; +import type { IDepartment } from './IDepartment'; + +export enum LivechatTransferEventType { + AGENT = 'agent', + DEPARTMENT = 'department', +} + +export interface ILivechatTransferEventContext { + type: LivechatTransferEventType; + room: IRoom; + from: IUser | IDepartment; + to: IUser | IDepartment; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts new file mode 100644 index 000000000000..5c322534c9ff --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentAssigned.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the assignment of a livechat agent. + */ +export interface IPostLivechatAgentAssigned { + /** + * Handler called *after* the assignment of a livechat agent. + * + * @param data the livechat context data which contains agent's info and room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_AGENT_ASSIGNED]( + context: ILivechatEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts new file mode 100644 index 000000000000..2884cfa94b83 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatAgentUnassigned.ts @@ -0,0 +1,25 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatEventContext } from './ILivechatEventContext'; + +/** + * Handler called after the unassignment of a livechat agent. + */ +export interface IPostLivechatAgentUnassigned { + /** + * Handler called *after* the unassignment of a livechat agent. + * + * @param data the livechat context data which contains agent's info and room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_AGENT_UNASSIGNED]( + context: ILivechatEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify?: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts new file mode 100644 index 000000000000..2c5edc0b9692 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatGuestSaved.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { IVisitor } from './IVisitor'; + +/** + * Handler called after the guest's info get saved. + */ +export interface IPostLivechatGuestSaved { + /** + * Handler called *after* the guest's info get saved. + * + * @param data the livechat context data which contains guest's info and room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_GUEST_SAVED](context: IVisitor, read: IRead, http: IHttp, persis: IPersistence, modify: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts new file mode 100644 index 000000000000..072e02e8c721 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomClosed.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after a livechat room is closed. + */ +export interface IPostLivechatRoomClosed { + /** + * Method called *after* a livechat room is closed. + * + * @param livechatRoom The livechat room which is closed. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_CLOSED](room: ILivechatRoom, read: IRead, http: IHttp, persis: IPersistence, modify?: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts new file mode 100644 index 000000000000..13b6d1cb2e4b --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomSaved.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after the room's info get saved. + */ +export interface IPostLivechatRoomSaved { + /** + * Handler called *after* the room's info get saved. + * + * @param data the livechat context data which contains room's info. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_SAVED](context: ILivechatRoom, read: IRead, http: IHttp, persis: IPersistence, modify: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts new file mode 100644 index 000000000000..237dcd9566e9 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomStarted.ts @@ -0,0 +1,19 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatRoom } from './ILivechatRoom'; + +/** + * Handler called after a livechat room is started. + */ +export interface IPostLivechatRoomStarted { + /** + * Method called *after* a livechat room is started. + * + * @param livechatRoom The livechat room which is started. + * @param read An accessor to the environment + * @param http An accessor to the outside world + * @param persis An accessor to the App's persistence + * @param modify An accessor to the modifier + */ + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_STARTED](room: ILivechatRoom, read: IRead, http: IHttp, persis: IPersistence, modify?: IModify): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts new file mode 100644 index 000000000000..aa86f8d358d3 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IPostLivechatRoomTransferred.ts @@ -0,0 +1,13 @@ +import type { IHttp, IModify, IPersistence, IRead } from '../accessors'; +import { AppMethod } from '../metadata'; +import type { ILivechatTransferEventContext } from './ILivechatTransferEventContext'; + +export interface IPostLivechatRoomTransferred { + [AppMethod.EXECUTE_POST_LIVECHAT_ROOM_TRANSFERRED]( + context: ILivechatTransferEventContext, + read: IRead, + http: IHttp, + persis: IPersistence, + modify: IModify, + ): Promise; +} diff --git a/packages/apps-engine/src/definition/livechat/IVisitor.ts b/packages/apps-engine/src/definition/livechat/IVisitor.ts new file mode 100644 index 000000000000..db5876dd912d --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IVisitor.ts @@ -0,0 +1,16 @@ +import type { IVisitorEmail } from './IVisitorEmail'; +import type { IVisitorPhone } from './IVisitorPhone'; + +export interface IVisitor { + id?: string; + token: string; + username: string; + updatedAt?: Date; + name: string; + department?: string; + phone?: Array; + visitorEmails?: Array; + status?: string; + customFields?: { [key: string]: any }; + livechatData?: { [key: string]: any }; +} diff --git a/packages/apps-engine/src/definition/livechat/IVisitorEmail.ts b/packages/apps-engine/src/definition/livechat/IVisitorEmail.ts new file mode 100644 index 000000000000..a1e35380666e --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IVisitorEmail.ts @@ -0,0 +1,3 @@ +export interface IVisitorEmail { + address: string; +} diff --git a/packages/apps-engine/src/definition/livechat/IVisitorPhone.ts b/packages/apps-engine/src/definition/livechat/IVisitorPhone.ts new file mode 100644 index 000000000000..fe112777e7d7 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/IVisitorPhone.ts @@ -0,0 +1,3 @@ +export interface IVisitorPhone { + phoneNumber: string; +} diff --git a/packages/apps-engine/src/definition/livechat/index.ts b/packages/apps-engine/src/definition/livechat/index.ts new file mode 100644 index 000000000000..5ea8d9f42885 --- /dev/null +++ b/packages/apps-engine/src/definition/livechat/index.ts @@ -0,0 +1,38 @@ +import { IDepartment } from './IDepartment'; +import { ILivechatEventContext } from './ILivechatEventContext'; +import { ILivechatMessage } from './ILivechatMessage'; +import { ILivechatRoom } from './ILivechatRoom'; +import { ILivechatRoomClosedHandler } from './ILivechatRoomClosedHandler'; +import { ILivechatTransferData } from './ILivechatTransferData'; +import { ILivechatTransferEventContext, LivechatTransferEventType } from './ILivechatTransferEventContext'; +import { IPostLivechatAgentAssigned } from './IPostLivechatAgentAssigned'; +import { IPostLivechatAgentUnassigned } from './IPostLivechatAgentUnassigned'; +import { IPostLivechatGuestSaved } from './IPostLivechatGuestSaved'; +import { IPostLivechatRoomClosed } from './IPostLivechatRoomClosed'; +import { IPostLivechatRoomSaved } from './IPostLivechatRoomSaved'; +import { IPostLivechatRoomStarted } from './IPostLivechatRoomStarted'; +import { IPostLivechatRoomTransferred } from './IPostLivechatRoomTransferred'; +import { IVisitor } from './IVisitor'; +import { IVisitorEmail } from './IVisitorEmail'; +import { IVisitorPhone } from './IVisitorPhone'; + +export { + ILivechatEventContext, + ILivechatMessage, + ILivechatRoom, + IPostLivechatAgentAssigned, + IPostLivechatAgentUnassigned, + IPostLivechatGuestSaved, + IPostLivechatRoomStarted, + IPostLivechatRoomClosed, + IPostLivechatRoomSaved, + IPostLivechatRoomTransferred, + ILivechatRoomClosedHandler, + ILivechatTransferData, + ILivechatTransferEventContext, + IDepartment, + IVisitor, + IVisitorEmail, + IVisitorPhone, + LivechatTransferEventType, +}; diff --git a/packages/apps-engine/src/definition/messages/IMessage.ts b/packages/apps-engine/src/definition/messages/IMessage.ts new file mode 100644 index 000000000000..d7ea6357497a --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IMessage.ts @@ -0,0 +1,34 @@ +import type { LayoutBlock } from '@rocket.chat/ui-kit'; + +import type { IRoom } from '../rooms'; +import type { IBlock } from '../uikit'; +import type { IUser, IUserLookup } from '../users'; +import type { IMessageAttachment } from './IMessageAttachment'; +import type { IMessageFile } from './IMessageFile'; +import type { IMessageReactions } from './IMessageReaction'; + +export interface IMessage { + id?: string; + threadId?: string; + room: IRoom; + sender: IUser; + text?: string; + createdAt?: Date; + updatedAt?: Date; + editor?: IUser; + editedAt?: Date; + emoji?: string; + avatarUrl?: string; + alias?: string; + file?: IMessageFile; + attachments?: Array; + reactions?: IMessageReactions; + groupable?: boolean; + parseUrls?: boolean; + customFields?: { [key: string]: any }; + blocks?: Array; + starred?: Array<{ _id: string }>; + pinned?: boolean; + pinnedAt?: Date; + pinnedBy?: IUserLookup; +} diff --git a/packages/apps-engine/src/definition/messages/IMessageAction.ts b/packages/apps-engine/src/definition/messages/IMessageAction.ts new file mode 100644 index 000000000000..3f32e4aa781d --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IMessageAction.ts @@ -0,0 +1,17 @@ +import type { MessageActionType } from './MessageActionType'; +import type { MessageProcessingType } from './MessageProcessingType'; + +/** + * Interface which represents an action which can be added to a message. + */ +export interface IMessageAction { + type: MessageActionType; + text?: string; + url?: string; + image_url?: string; + is_webview?: boolean; + webview_height_ratio?: string; + msg?: string; + msg_in_chat_window?: boolean; + msg_processing_type?: MessageProcessingType; +} diff --git a/packages/apps-engine/src/definition/messages/IMessageAttachment.ts b/packages/apps-engine/src/definition/messages/IMessageAttachment.ts new file mode 100644 index 000000000000..96dd8aa1fe34 --- /dev/null +++ b/packages/apps-engine/src/definition/messages/IMessageAttachment.ts @@ -0,0 +1,43 @@ +import type { IMessageAction } from './IMessageAction'; +import type { IMessageAttachmentAuthor } from './IMessageAttachmentAuthor'; +import type { IMessageAttachmentField } from './IMessageAttachmentField'; +import type { IMessageAttachmentTitle } from './IMessageAttachmentTitle'; +import type { MessageActionButtonsAlignment } from './MessageActionButtonsAlignment'; + +/** + * Interface which represents an attachment which can be added to a message. + */ +export interface IMessageAttachment { + /** Causes the image, audio, and video sections to be hidding when this is true. */ + collapsed?: boolean; + /** The color you want the order on the left side to be, supports any valid background-css value. */ + color?: string; // TODO: Maybe we change this to a Color class which has helper methods? + /** The text to display for this attachment. */ + text?: string; + /** Displays the time next to the text portion. */ + timestamp?: Date; + /** Only applicable if the timestamp is provided, as it makes the time clickable to this link. */ + timestampLink?: string; + /** An image that displays to the left of the text, looks better when this is relatively small. */ + thumbnailUrl?: string; + /** Author portion of the attachment. */ + author?: IMessageAttachmentAuthor; + /** Title portion of the attachment. */ + title?: IMessageAttachmentTitle; + /** The image to display, will be "big" and easy to see. */ + imageUrl?: string; + /** Audio file to play, only supports what html's